2022-02-23

This commit is contained in:
Daniel S. 2022-02-23 22:45:59 +01:00
parent 35a0c40d14
commit dc68cce9ed
80 changed files with 859345 additions and 4387 deletions

1
.gitignore vendored
View File

@ -26,3 +26,4 @@ ed_lrr_gui/web/ed_lrr_web_ui.db
__version__.py
.nox/
dist_vis.py
img/**

View File

@ -0,0 +1,6 @@
{
"cells": [],
"metadata": {},
"nbformat": 4,
"nbformat_minor": 4
}

View File

@ -0,0 +1,904 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Populating the interactive namespace from numpy and matplotlib\n"
]
}
],
"source": [
"%pylab notebook"
]
},
{
"cell_type": "code",
"execution_count": 131,
"metadata": {},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"<ipython-input-131-1f27bb9e71ac>:5: RuntimeWarning: invalid value encountered in double_scalars\n",
" ld = (((a+b+c)*(-a+b+c)*(a-b+c)*(a+b-c))**0.5)/(c*2.0)\n"
]
},
{
"data": {
"application/javascript": [
"/* Put everything inside the global mpl namespace */\n",
"window.mpl = {};\n",
"\n",
"\n",
"mpl.get_websocket_type = function() {\n",
" if (typeof(WebSocket) !== 'undefined') {\n",
" return WebSocket;\n",
" } else if (typeof(MozWebSocket) !== 'undefined') {\n",
" return MozWebSocket;\n",
" } else {\n",
" alert('Your browser does not have WebSocket support. ' +\n",
" 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n",
" 'Firefox 4 and 5 are also supported but you ' +\n",
" 'have to enable WebSockets in about:config.');\n",
" };\n",
"}\n",
"\n",
"mpl.figure = function(figure_id, websocket, ondownload, parent_element) {\n",
" this.id = figure_id;\n",
"\n",
" this.ws = websocket;\n",
"\n",
" this.supports_binary = (this.ws.binaryType != undefined);\n",
"\n",
" if (!this.supports_binary) {\n",
" var warnings = document.getElementById(\"mpl-warnings\");\n",
" if (warnings) {\n",
" warnings.style.display = 'block';\n",
" warnings.textContent = (\n",
" \"This browser does not support binary websocket messages. \" +\n",
" \"Performance may be slow.\");\n",
" }\n",
" }\n",
"\n",
" this.imageObj = new Image();\n",
"\n",
" this.context = undefined;\n",
" this.message = undefined;\n",
" this.canvas = undefined;\n",
" this.rubberband_canvas = undefined;\n",
" this.rubberband_context = undefined;\n",
" this.format_dropdown = undefined;\n",
"\n",
" this.image_mode = 'full';\n",
"\n",
" this.root = $('<div/>');\n",
" this._root_extra_style(this.root)\n",
" this.root.attr('style', 'display: inline-block');\n",
"\n",
" $(parent_element).append(this.root);\n",
"\n",
" this._init_header(this);\n",
" this._init_canvas(this);\n",
" this._init_toolbar(this);\n",
"\n",
" var fig = this;\n",
"\n",
" this.waiting = false;\n",
"\n",
" this.ws.onopen = function () {\n",
" fig.send_message(\"supports_binary\", {value: fig.supports_binary});\n",
" fig.send_message(\"send_image_mode\", {});\n",
" if (mpl.ratio != 1) {\n",
" fig.send_message(\"set_dpi_ratio\", {'dpi_ratio': mpl.ratio});\n",
" }\n",
" fig.send_message(\"refresh\", {});\n",
" }\n",
"\n",
" this.imageObj.onload = function() {\n",
" if (fig.image_mode == 'full') {\n",
" // Full images could contain transparency (where diff images\n",
" // almost always do), so we need to clear the canvas so that\n",
" // there is no ghosting.\n",
" fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n",
" }\n",
" fig.context.drawImage(fig.imageObj, 0, 0);\n",
" };\n",
"\n",
" this.imageObj.onunload = function() {\n",
" fig.ws.close();\n",
" }\n",
"\n",
" this.ws.onmessage = this._make_on_message_function(this);\n",
"\n",
" this.ondownload = ondownload;\n",
"}\n",
"\n",
"mpl.figure.prototype._init_header = function() {\n",
" var titlebar = $(\n",
" '<div class=\"ui-dialog-titlebar ui-widget-header ui-corner-all ' +\n",
" 'ui-helper-clearfix\"/>');\n",
" var titletext = $(\n",
" '<div class=\"ui-dialog-title\" style=\"width: 100%; ' +\n",
" 'text-align: center; padding: 3px;\"/>');\n",
" titlebar.append(titletext)\n",
" this.root.append(titlebar);\n",
" this.header = titletext[0];\n",
"}\n",
"\n",
"\n",
"\n",
"mpl.figure.prototype._canvas_extra_style = function(canvas_div) {\n",
"\n",
"}\n",
"\n",
"\n",
"mpl.figure.prototype._root_extra_style = function(canvas_div) {\n",
"\n",
"}\n",
"\n",
"mpl.figure.prototype._init_canvas = function() {\n",
" var fig = this;\n",
"\n",
" var canvas_div = $('<div/>');\n",
"\n",
" canvas_div.attr('style', 'position: relative; clear: both; outline: 0');\n",
"\n",
" function canvas_keyboard_event(event) {\n",
" return fig.key_event(event, event['data']);\n",
" }\n",
"\n",
" canvas_div.keydown('key_press', canvas_keyboard_event);\n",
" canvas_div.keyup('key_release', canvas_keyboard_event);\n",
" this.canvas_div = canvas_div\n",
" this._canvas_extra_style(canvas_div)\n",
" this.root.append(canvas_div);\n",
"\n",
" var canvas = $('<canvas/>');\n",
" canvas.addClass('mpl-canvas');\n",
" canvas.attr('style', \"left: 0; top: 0; z-index: 0; outline: 0\")\n",
"\n",
" this.canvas = canvas[0];\n",
" this.context = canvas[0].getContext(\"2d\");\n",
"\n",
" var backingStore = this.context.backingStorePixelRatio ||\n",
"\tthis.context.webkitBackingStorePixelRatio ||\n",
"\tthis.context.mozBackingStorePixelRatio ||\n",
"\tthis.context.msBackingStorePixelRatio ||\n",
"\tthis.context.oBackingStorePixelRatio ||\n",
"\tthis.context.backingStorePixelRatio || 1;\n",
"\n",
" mpl.ratio = (window.devicePixelRatio || 1) / backingStore;\n",
"\n",
" var rubberband = $('<canvas/>');\n",
" rubberband.attr('style', \"position: absolute; left: 0; top: 0; z-index: 1;\")\n",
"\n",
" var pass_mouse_events = true;\n",
"\n",
" canvas_div.resizable({\n",
" start: function(event, ui) {\n",
" pass_mouse_events = false;\n",
" },\n",
" resize: function(event, ui) {\n",
" fig.request_resize(ui.size.width, ui.size.height);\n",
" },\n",
" stop: function(event, ui) {\n",
" pass_mouse_events = true;\n",
" fig.request_resize(ui.size.width, ui.size.height);\n",
" },\n",
" });\n",
"\n",
" function mouse_event_fn(event) {\n",
" if (pass_mouse_events)\n",
" return fig.mouse_event(event, event['data']);\n",
" }\n",
"\n",
" rubberband.mousedown('button_press', mouse_event_fn);\n",
" rubberband.mouseup('button_release', mouse_event_fn);\n",
" // Throttle sequential mouse events to 1 every 20ms.\n",
" rubberband.mousemove('motion_notify', mouse_event_fn);\n",
"\n",
" rubberband.mouseenter('figure_enter', mouse_event_fn);\n",
" rubberband.mouseleave('figure_leave', mouse_event_fn);\n",
"\n",
" canvas_div.on(\"wheel\", function (event) {\n",
" event = event.originalEvent;\n",
" event['data'] = 'scroll'\n",
" if (event.deltaY < 0) {\n",
" event.step = 1;\n",
" } else {\n",
" event.step = -1;\n",
" }\n",
" mouse_event_fn(event);\n",
" });\n",
"\n",
" canvas_div.append(canvas);\n",
" canvas_div.append(rubberband);\n",
"\n",
" this.rubberband = rubberband;\n",
" this.rubberband_canvas = rubberband[0];\n",
" this.rubberband_context = rubberband[0].getContext(\"2d\");\n",
" this.rubberband_context.strokeStyle = \"#000000\";\n",
"\n",
" this._resize_canvas = function(width, height) {\n",
" // Keep the size of the canvas, canvas container, and rubber band\n",
" // canvas in synch.\n",
" canvas_div.css('width', width)\n",
" canvas_div.css('height', height)\n",
"\n",
" canvas.attr('width', width * mpl.ratio);\n",
" canvas.attr('height', height * mpl.ratio);\n",
" canvas.attr('style', 'width: ' + width + 'px; height: ' + height + 'px;');\n",
"\n",
" rubberband.attr('width', width);\n",
" rubberband.attr('height', height);\n",
" }\n",
"\n",
" // Set the figure to an initial 600x600px, this will subsequently be updated\n",
" // upon first draw.\n",
" this._resize_canvas(600, 600);\n",
"\n",
" // Disable right mouse context menu.\n",
" $(this.rubberband_canvas).bind(\"contextmenu\",function(e){\n",
" return false;\n",
" });\n",
"\n",
" function set_focus () {\n",
" canvas.focus();\n",
" canvas_div.focus();\n",
" }\n",
"\n",
" window.setTimeout(set_focus, 100);\n",
"}\n",
"\n",
"mpl.figure.prototype._init_toolbar = function() {\n",
" var fig = this;\n",
"\n",
" var nav_element = $('<div/>');\n",
" nav_element.attr('style', 'width: 100%');\n",
" this.root.append(nav_element);\n",
"\n",
" // Define a callback function for later on.\n",
" function toolbar_event(event) {\n",
" return fig.toolbar_button_onclick(event['data']);\n",
" }\n",
" function toolbar_mouse_event(event) {\n",
" return fig.toolbar_button_onmouseover(event['data']);\n",
" }\n",
"\n",
" for(var toolbar_ind in mpl.toolbar_items) {\n",
" var name = mpl.toolbar_items[toolbar_ind][0];\n",
" var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
" var image = mpl.toolbar_items[toolbar_ind][2];\n",
" var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
"\n",
" if (!name) {\n",
" // put a spacer in here.\n",
" continue;\n",
" }\n",
" var button = $('<button/>');\n",
" button.addClass('ui-button ui-widget ui-state-default ui-corner-all ' +\n",
" 'ui-button-icon-only');\n",
" button.attr('role', 'button');\n",
" button.attr('aria-disabled', 'false');\n",
" button.click(method_name, toolbar_event);\n",
" button.mouseover(tooltip, toolbar_mouse_event);\n",
"\n",
" var icon_img = $('<span/>');\n",
" icon_img.addClass('ui-button-icon-primary ui-icon');\n",
" icon_img.addClass(image);\n",
" icon_img.addClass('ui-corner-all');\n",
"\n",
" var tooltip_span = $('<span/>');\n",
" tooltip_span.addClass('ui-button-text');\n",
" tooltip_span.html(tooltip);\n",
"\n",
" button.append(icon_img);\n",
" button.append(tooltip_span);\n",
"\n",
" nav_element.append(button);\n",
" }\n",
"\n",
" var fmt_picker_span = $('<span/>');\n",
"\n",
" var fmt_picker = $('<select/>');\n",
" fmt_picker.addClass('mpl-toolbar-option ui-widget ui-widget-content');\n",
" fmt_picker_span.append(fmt_picker);\n",
" nav_element.append(fmt_picker_span);\n",
" this.format_dropdown = fmt_picker[0];\n",
"\n",
" for (var ind in mpl.extensions) {\n",
" var fmt = mpl.extensions[ind];\n",
" var option = $(\n",
" '<option/>', {selected: fmt === mpl.default_extension}).html(fmt);\n",
" fmt_picker.append(option);\n",
" }\n",
"\n",
" // Add hover states to the ui-buttons\n",
" $( \".ui-button\" ).hover(\n",
" function() { $(this).addClass(\"ui-state-hover\");},\n",
" function() { $(this).removeClass(\"ui-state-hover\");}\n",
" );\n",
"\n",
" var status_bar = $('<span class=\"mpl-message\"/>');\n",
" nav_element.append(status_bar);\n",
" this.message = status_bar[0];\n",
"}\n",
"\n",
"mpl.figure.prototype.request_resize = function(x_pixels, y_pixels) {\n",
" // Request matplotlib to resize the figure. Matplotlib will then trigger a resize in the client,\n",
" // which will in turn request a refresh of the image.\n",
" this.send_message('resize', {'width': x_pixels, 'height': y_pixels});\n",
"}\n",
"\n",
"mpl.figure.prototype.send_message = function(type, properties) {\n",
" properties['type'] = type;\n",
" properties['figure_id'] = this.id;\n",
" this.ws.send(JSON.stringify(properties));\n",
"}\n",
"\n",
"mpl.figure.prototype.send_draw_message = function() {\n",
" if (!this.waiting) {\n",
" this.waiting = true;\n",
" this.ws.send(JSON.stringify({type: \"draw\", figure_id: this.id}));\n",
" }\n",
"}\n",
"\n",
"\n",
"mpl.figure.prototype.handle_save = function(fig, msg) {\n",
" var format_dropdown = fig.format_dropdown;\n",
" var format = format_dropdown.options[format_dropdown.selectedIndex].value;\n",
" fig.ondownload(fig, format);\n",
"}\n",
"\n",
"\n",
"mpl.figure.prototype.handle_resize = function(fig, msg) {\n",
" var size = msg['size'];\n",
" if (size[0] != fig.canvas.width || size[1] != fig.canvas.height) {\n",
" fig._resize_canvas(size[0], size[1]);\n",
" fig.send_message(\"refresh\", {});\n",
" };\n",
"}\n",
"\n",
"mpl.figure.prototype.handle_rubberband = function(fig, msg) {\n",
" var x0 = msg['x0'] / mpl.ratio;\n",
" var y0 = (fig.canvas.height - msg['y0']) / mpl.ratio;\n",
" var x1 = msg['x1'] / mpl.ratio;\n",
" var y1 = (fig.canvas.height - msg['y1']) / mpl.ratio;\n",
" x0 = Math.floor(x0) + 0.5;\n",
" y0 = Math.floor(y0) + 0.5;\n",
" x1 = Math.floor(x1) + 0.5;\n",
" y1 = Math.floor(y1) + 0.5;\n",
" var min_x = Math.min(x0, x1);\n",
" var min_y = Math.min(y0, y1);\n",
" var width = Math.abs(x1 - x0);\n",
" var height = Math.abs(y1 - y0);\n",
"\n",
" fig.rubberband_context.clearRect(\n",
" 0, 0, fig.canvas.width / mpl.ratio, fig.canvas.height / mpl.ratio);\n",
"\n",
" fig.rubberband_context.strokeRect(min_x, min_y, width, height);\n",
"}\n",
"\n",
"mpl.figure.prototype.handle_figure_label = function(fig, msg) {\n",
" // Updates the figure title.\n",
" fig.header.textContent = msg['label'];\n",
"}\n",
"\n",
"mpl.figure.prototype.handle_cursor = function(fig, msg) {\n",
" var cursor = msg['cursor'];\n",
" switch(cursor)\n",
" {\n",
" case 0:\n",
" cursor = 'pointer';\n",
" break;\n",
" case 1:\n",
" cursor = 'default';\n",
" break;\n",
" case 2:\n",
" cursor = 'crosshair';\n",
" break;\n",
" case 3:\n",
" cursor = 'move';\n",
" break;\n",
" }\n",
" fig.rubberband_canvas.style.cursor = cursor;\n",
"}\n",
"\n",
"mpl.figure.prototype.handle_message = function(fig, msg) {\n",
" fig.message.textContent = msg['message'];\n",
"}\n",
"\n",
"mpl.figure.prototype.handle_draw = function(fig, msg) {\n",
" // Request the server to send over a new figure.\n",
" fig.send_draw_message();\n",
"}\n",
"\n",
"mpl.figure.prototype.handle_image_mode = function(fig, msg) {\n",
" fig.image_mode = msg['mode'];\n",
"}\n",
"\n",
"mpl.figure.prototype.updated_canvas_event = function() {\n",
" // Called whenever the canvas gets updated.\n",
" this.send_message(\"ack\", {});\n",
"}\n",
"\n",
"// A function to construct a web socket function for onmessage handling.\n",
"// Called in the figure constructor.\n",
"mpl.figure.prototype._make_on_message_function = function(fig) {\n",
" return function socket_on_message(evt) {\n",
" if (evt.data instanceof Blob) {\n",
" /* FIXME: We get \"Resource interpreted as Image but\n",
" * transferred with MIME type text/plain:\" errors on\n",
" * Chrome. But how to set the MIME type? It doesn't seem\n",
" * to be part of the websocket stream */\n",
" evt.data.type = \"image/png\";\n",
"\n",
" /* Free the memory for the previous frames */\n",
" if (fig.imageObj.src) {\n",
" (window.URL || window.webkitURL).revokeObjectURL(\n",
" fig.imageObj.src);\n",
" }\n",
"\n",
" fig.imageObj.src = (window.URL || window.webkitURL).createObjectURL(\n",
" evt.data);\n",
" fig.updated_canvas_event();\n",
" fig.waiting = false;\n",
" return;\n",
" }\n",
" else if (typeof evt.data === 'string' && evt.data.slice(0, 21) == \"data:image/png;base64\") {\n",
" fig.imageObj.src = evt.data;\n",
" fig.updated_canvas_event();\n",
" fig.waiting = false;\n",
" return;\n",
" }\n",
"\n",
" var msg = JSON.parse(evt.data);\n",
" var msg_type = msg['type'];\n",
"\n",
" // Call the \"handle_{type}\" callback, which takes\n",
" // the figure and JSON message as its only arguments.\n",
" try {\n",
" var callback = fig[\"handle_\" + msg_type];\n",
" } catch (e) {\n",
" console.log(\"No handler for the '\" + msg_type + \"' message type: \", msg);\n",
" return;\n",
" }\n",
"\n",
" if (callback) {\n",
" try {\n",
" // console.log(\"Handling '\" + msg_type + \"' message: \", msg);\n",
" callback(fig, msg);\n",
" } catch (e) {\n",
" console.log(\"Exception inside the 'handler_\" + msg_type + \"' callback:\", e, e.stack, msg);\n",
" }\n",
" }\n",
" };\n",
"}\n",
"\n",
"// from http://stackoverflow.com/questions/1114465/getting-mouse-location-in-canvas\n",
"mpl.findpos = function(e) {\n",
" //this section is from http://www.quirksmode.org/js/events_properties.html\n",
" var targ;\n",
" if (!e)\n",
" e = window.event;\n",
" if (e.target)\n",
" targ = e.target;\n",
" else if (e.srcElement)\n",
" targ = e.srcElement;\n",
" if (targ.nodeType == 3) // defeat Safari bug\n",
" targ = targ.parentNode;\n",
"\n",
" // jQuery normalizes the pageX and pageY\n",
" // pageX,Y are the mouse positions relative to the document\n",
" // offset() returns the position of the element relative to the document\n",
" var x = e.pageX - $(targ).offset().left;\n",
" var y = e.pageY - $(targ).offset().top;\n",
"\n",
" return {\"x\": x, \"y\": y};\n",
"};\n",
"\n",
"/*\n",
" * return a copy of an object with only non-object keys\n",
" * we need this to avoid circular references\n",
" * http://stackoverflow.com/a/24161582/3208463\n",
" */\n",
"function simpleKeys (original) {\n",
" return Object.keys(original).reduce(function (obj, key) {\n",
" if (typeof original[key] !== 'object')\n",
" obj[key] = original[key]\n",
" return obj;\n",
" }, {});\n",
"}\n",
"\n",
"mpl.figure.prototype.mouse_event = function(event, name) {\n",
" var canvas_pos = mpl.findpos(event)\n",
"\n",
" if (name === 'button_press')\n",
" {\n",
" this.canvas.focus();\n",
" this.canvas_div.focus();\n",
" }\n",
"\n",
" var x = canvas_pos.x * mpl.ratio;\n",
" var y = canvas_pos.y * mpl.ratio;\n",
"\n",
" this.send_message(name, {x: x, y: y, button: event.button,\n",
" step: event.step,\n",
" guiEvent: simpleKeys(event)});\n",
"\n",
" /* This prevents the web browser from automatically changing to\n",
" * the text insertion cursor when the button is pressed. We want\n",
" * to control all of the cursor setting manually through the\n",
" * 'cursor' event from matplotlib */\n",
" event.preventDefault();\n",
" return false;\n",
"}\n",
"\n",
"mpl.figure.prototype._key_event_extra = function(event, name) {\n",
" // Handle any extra behaviour associated with a key event\n",
"}\n",
"\n",
"mpl.figure.prototype.key_event = function(event, name) {\n",
"\n",
" // Prevent repeat events\n",
" if (name == 'key_press')\n",
" {\n",
" if (event.which === this._key)\n",
" return;\n",
" else\n",
" this._key = event.which;\n",
" }\n",
" if (name == 'key_release')\n",
" this._key = null;\n",
"\n",
" var value = '';\n",
" if (event.ctrlKey && event.which != 17)\n",
" value += \"ctrl+\";\n",
" if (event.altKey && event.which != 18)\n",
" value += \"alt+\";\n",
" if (event.shiftKey && event.which != 16)\n",
" value += \"shift+\";\n",
"\n",
" value += 'k';\n",
" value += event.which.toString();\n",
"\n",
" this._key_event_extra(event, name);\n",
"\n",
" this.send_message(name, {key: value,\n",
" guiEvent: simpleKeys(event)});\n",
" return false;\n",
"}\n",
"\n",
"mpl.figure.prototype.toolbar_button_onclick = function(name) {\n",
" if (name == 'download') {\n",
" this.handle_save(this, null);\n",
" } else {\n",
" this.send_message(\"toolbar_button\", {name: name});\n",
" }\n",
"};\n",
"\n",
"mpl.figure.prototype.toolbar_button_onmouseover = function(tooltip) {\n",
" this.message.textContent = tooltip;\n",
"};\n",
"mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home icon-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left icon-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right icon-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Pan axes with left mouse, zoom with right\", \"fa fa-arrows icon-move\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\", \"fa fa-square-o icon-check-empty\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o icon-save\", \"download\"]];\n",
"\n",
"mpl.extensions = [\"eps\", \"jpeg\", \"pdf\", \"png\", \"ps\", \"raw\", \"svg\", \"tif\"];\n",
"\n",
"mpl.default_extension = \"png\";var comm_websocket_adapter = function(comm) {\n",
" // Create a \"websocket\"-like object which calls the given IPython comm\n",
" // object with the appropriate methods. Currently this is a non binary\n",
" // socket, so there is still some room for performance tuning.\n",
" var ws = {};\n",
"\n",
" ws.close = function() {\n",
" comm.close()\n",
" };\n",
" ws.send = function(m) {\n",
" //console.log('sending', m);\n",
" comm.send(m);\n",
" };\n",
" // Register the callback with on_msg.\n",
" comm.on_msg(function(msg) {\n",
" //console.log('receiving', msg['content']['data'], msg);\n",
" // Pass the mpl event to the overridden (by mpl) onmessage function.\n",
" ws.onmessage(msg['content']['data'])\n",
" });\n",
" return ws;\n",
"}\n",
"\n",
"mpl.mpl_figure_comm = function(comm, msg) {\n",
" // This is the function which gets called when the mpl process\n",
" // starts-up an IPython Comm through the \"matplotlib\" channel.\n",
"\n",
" var id = msg.content.data.id;\n",
" // Get hold of the div created by the display call when the Comm\n",
" // socket was opened in Python.\n",
" var element = $(\"#\" + id);\n",
" var ws_proxy = comm_websocket_adapter(comm)\n",
"\n",
" function ondownload(figure, format) {\n",
" window.open(figure.imageObj.src);\n",
" }\n",
"\n",
" var fig = new mpl.figure(id, ws_proxy,\n",
" ondownload,\n",
" element.get(0));\n",
"\n",
" // Call onopen now - mpl needs it, as it is assuming we've passed it a real\n",
" // web socket which is closed, not our websocket->open comm proxy.\n",
" ws_proxy.onopen();\n",
"\n",
" fig.parent_element = element.get(0);\n",
" fig.cell_info = mpl.find_output_cell(\"<div id='\" + id + \"'></div>\");\n",
" if (!fig.cell_info) {\n",
" console.error(\"Failed to find cell for figure\", id, fig);\n",
" return;\n",
" }\n",
"\n",
" var output_index = fig.cell_info[2]\n",
" var cell = fig.cell_info[0];\n",
"\n",
"};\n",
"\n",
"mpl.figure.prototype.handle_close = function(fig, msg) {\n",
" var width = fig.canvas.width/mpl.ratio\n",
" fig.root.unbind('remove')\n",
"\n",
" // Update the output cell to use the data from the current canvas.\n",
" fig.push_to_output();\n",
" var dataURL = fig.canvas.toDataURL();\n",
" // Re-enable the keyboard manager in IPython - without this line, in FF,\n",
" // the notebook keyboard shortcuts fail.\n",
" IPython.keyboard_manager.enable()\n",
" $(fig.parent_element).html('<img src=\"' + dataURL + '\" width=\"' + width + '\">');\n",
" fig.close_ws(fig, msg);\n",
"}\n",
"\n",
"mpl.figure.prototype.close_ws = function(fig, msg){\n",
" fig.send_message('closing', msg);\n",
" // fig.ws.close()\n",
"}\n",
"\n",
"mpl.figure.prototype.push_to_output = function(remove_interactive) {\n",
" // Turn the data on the canvas into data in the output cell.\n",
" var width = this.canvas.width/mpl.ratio\n",
" var dataURL = this.canvas.toDataURL();\n",
" this.cell_info[1]['text/html'] = '<img src=\"' + dataURL + '\" width=\"' + width + '\">';\n",
"}\n",
"\n",
"mpl.figure.prototype.updated_canvas_event = function() {\n",
" // Tell IPython that the notebook contents must change.\n",
" IPython.notebook.set_dirty(true);\n",
" this.send_message(\"ack\", {});\n",
" var fig = this;\n",
" // Wait a second, then push the new image to the DOM so\n",
" // that it is saved nicely (might be nice to debounce this).\n",
" setTimeout(function () { fig.push_to_output() }, 1000);\n",
"}\n",
"\n",
"mpl.figure.prototype._init_toolbar = function() {\n",
" var fig = this;\n",
"\n",
" var nav_element = $('<div/>');\n",
" nav_element.attr('style', 'width: 100%');\n",
" this.root.append(nav_element);\n",
"\n",
" // Define a callback function for later on.\n",
" function toolbar_event(event) {\n",
" return fig.toolbar_button_onclick(event['data']);\n",
" }\n",
" function toolbar_mouse_event(event) {\n",
" return fig.toolbar_button_onmouseover(event['data']);\n",
" }\n",
"\n",
" for(var toolbar_ind in mpl.toolbar_items){\n",
" var name = mpl.toolbar_items[toolbar_ind][0];\n",
" var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
" var image = mpl.toolbar_items[toolbar_ind][2];\n",
" var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
"\n",
" if (!name) { continue; };\n",
"\n",
" var button = $('<button class=\"btn btn-default\" href=\"#\" title=\"' + name + '\"><i class=\"fa ' + image + ' fa-lg\"></i></button>');\n",
" button.click(method_name, toolbar_event);\n",
" button.mouseover(tooltip, toolbar_mouse_event);\n",
" nav_element.append(button);\n",
" }\n",
"\n",
" // Add the status bar.\n",
" var status_bar = $('<span class=\"mpl-message\" style=\"text-align:right; float: right;\"/>');\n",
" nav_element.append(status_bar);\n",
" this.message = status_bar[0];\n",
"\n",
" // Add the close button to the window.\n",
" var buttongrp = $('<div class=\"btn-group inline pull-right\"></div>');\n",
" var button = $('<button class=\"btn btn-mini btn-primary\" href=\"#\" title=\"Stop Interaction\"><i class=\"fa fa-power-off icon-remove icon-large\"></i></button>');\n",
" button.click(function (evt) { fig.handle_close(fig, {}); } );\n",
" button.mouseover('Stop Interaction', toolbar_mouse_event);\n",
" buttongrp.append(button);\n",
" var titlebar = this.root.find($('.ui-dialog-titlebar'));\n",
" titlebar.prepend(buttongrp);\n",
"}\n",
"\n",
"mpl.figure.prototype._root_extra_style = function(el){\n",
" var fig = this\n",
" el.on(\"remove\", function(){\n",
"\tfig.close_ws(fig, {});\n",
" });\n",
"}\n",
"\n",
"mpl.figure.prototype._canvas_extra_style = function(el){\n",
" // this is important to make the div 'focusable\n",
" el.attr('tabindex', 0)\n",
" // reach out to IPython and tell the keyboard manager to turn it's self\n",
" // off when our div gets focus\n",
"\n",
" // location in version 3\n",
" if (IPython.notebook.keyboard_manager) {\n",
" IPython.notebook.keyboard_manager.register_events(el);\n",
" }\n",
" else {\n",
" // location in version 2\n",
" IPython.keyboard_manager.register_events(el);\n",
" }\n",
"\n",
"}\n",
"\n",
"mpl.figure.prototype._key_event_extra = function(event, name) {\n",
" var manager = IPython.notebook.keyboard_manager;\n",
" if (!manager)\n",
" manager = IPython.keyboard_manager;\n",
"\n",
" // Check for shift+enter\n",
" if (event.shiftKey && event.which == 13) {\n",
" this.canvas_div.blur();\n",
" // select the cell after this one\n",
" var index = IPython.notebook.find_cell_index(this.cell_info[0]);\n",
" IPython.notebook.select(index + 1);\n",
" }\n",
"}\n",
"\n",
"mpl.figure.prototype.handle_save = function(fig, msg) {\n",
" fig.ondownload(fig, null);\n",
"}\n",
"\n",
"\n",
"mpl.find_output_cell = function(html_output) {\n",
" // Return the cell and output element which can be found *uniquely* in the notebook.\n",
" // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n",
" // IPython event is triggered only after the cells have been serialised, which for\n",
" // our purposes (turning an active figure into a static one), is too late.\n",
" var cells = IPython.notebook.get_cells();\n",
" var ncells = cells.length;\n",
" for (var i=0; i<ncells; i++) {\n",
" var cell = cells[i];\n",
" if (cell.cell_type === 'code'){\n",
" for (var j=0; j<cell.output_area.outputs.length; j++) {\n",
" var data = cell.output_area.outputs[j];\n",
" if (data.data) {\n",
" // IPython >= 3 moved mimebundle to data attribute of output\n",
" data = data.data;\n",
" }\n",
" if (data['text/html'] == html_output) {\n",
" return [cell, data, j];\n",
" }\n",
" }\n",
" }\n",
" }\n",
"}\n",
"\n",
"// Register the function which deals with the matplotlib target/channel.\n",
"// The kernel may be null if the page has been refreshed.\n",
"if (IPython.notebook.kernel != null) {\n",
" IPython.notebook.kernel.comm_manager.register_target('matplotlib', mpl.mpl_figure_comm);\n",
"}\n"
],
"text/plain": [
"<IPython.core.display.Javascript object>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"text/html": [
"<div id='215b26ff-80fe-45a3-bb52-9351ea1b2ebb'></div>"
],
"text/plain": [
"<IPython.core.display.HTML object>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"text/plain": [
"(0, 100)"
]
},
"execution_count": 131,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"def h(p,src,dest):\n",
" c = (((src-dest)**2).sum())**0.5\n",
" a = (((dest-p)**2).sum())**0.5\n",
" b = (((src-p)**2).sum())**0.5\n",
" ld = (((a+b+c)*(-a+b+c)*(a-b+c)*(a+b-c))**0.5)/(c*2.0)\n",
" return a-b\n",
"S=np.array([10,10])\n",
"D=np.array([20,25])\n",
"\n",
"Rx=range(100)\n",
"Ry=range(100)\n",
"grid=np.zeros((len(Rx),len(Ry)))\n",
"for px,x in enumerate(Rx):\n",
" for py,y in enumerate(Ry):\n",
" grid[px,py]=h(np.array([x,y]),S,D)\n",
"imshow(grid,cmap='coolwarm_r',origin='lower')\n",
"colorbar()\n",
"\n",
"scatter(*S,color='green')\n",
"scatter(*D,color='red')\n",
"plt.xlim(0,100)\n",
"plt.ylim(0,100)"
]
},
{
"cell_type": "code",
"execution_count": 38,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Object `np.magnitude` not found.\n"
]
}
],
"source": [
"np."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3.8.1 64-bit ('anaconda': conda)",
"language": "python",
"name": "python38164bitanacondaconda2a51168e890d45bd836f654eb2ae46f7"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.8.1"
}
},
"nbformat": 4,
"nbformat_minor": 4
}

View File

@ -0,0 +1,505 @@
# -*- coding: utf-8 -*-
################################################################################
## Form generated from reading UI file 'ed_lrr.ui'
##
## Created by: Qt User Interface Compiler version 5.15.2
##
## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################
from PySide2.QtCore import *
from PySide2.QtGui import *
from PySide2.QtWidgets import *
class Ui_ED_LRR(object):
def setupUi(self, ED_LRR):
if not ED_LRR.objectName():
ED_LRR.setObjectName(u"ED_LRR")
ED_LRR.setEnabled(True)
ED_LRR.resize(577, 500)
sizePolicy = QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(ED_LRR.sizePolicy().hasHeightForWidth())
ED_LRR.setSizePolicy(sizePolicy)
ED_LRR.setMinimumSize(QSize(577, 500))
ED_LRR.setMaximumSize(QSize(577, 500))
ED_LRR.setStyleSheet(u"")
ED_LRR.setDocumentMode(False)
ED_LRR.setTabShape(QTabWidget.Rounded)
self.menu_act_quit = QAction(ED_LRR)
self.menu_act_quit.setObjectName(u"menu_act_quit")
self.actionA = QAction(ED_LRR)
self.actionA.setObjectName(u"actionA")
self.actionB = QAction(ED_LRR)
self.actionB.setObjectName(u"actionB")
self.centralwidget = QWidget(ED_LRR)
self.centralwidget.setObjectName(u"centralwidget")
sizePolicy1 = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
sizePolicy1.setHorizontalStretch(0)
sizePolicy1.setVerticalStretch(0)
sizePolicy1.setHeightForWidth(self.centralwidget.sizePolicy().hasHeightForWidth())
self.centralwidget.setSizePolicy(sizePolicy1)
self.verticalLayout = QVBoxLayout(self.centralwidget)
self.verticalLayout.setObjectName(u"verticalLayout")
self.tabs = QTabWidget(self.centralwidget)
self.tabs.setObjectName(u"tabs")
self.tabs.setEnabled(True)
self.tabs.setAutoFillBackground(False)
self.tabs.setTabPosition(QTabWidget.North)
self.tabs.setTabShape(QTabWidget.Rounded)
self.tabs.setElideMode(Qt.ElideNone)
self.tabs.setTabsClosable(False)
self.tabs.setTabBarAutoHide(False)
self.tab_download = QWidget()
self.tab_download.setObjectName(u"tab_download")
self.formLayout = QFormLayout(self.tab_download)
self.formLayout.setObjectName(u"formLayout")
self.lbl_bodies_dl = QLabel(self.tab_download)
self.lbl_bodies_dl.setObjectName(u"lbl_bodies_dl")
self.formLayout.setWidget(1, QFormLayout.LabelRole, self.lbl_bodies_dl)
self.lbl_systems_dl = QLabel(self.tab_download)
self.lbl_systems_dl.setObjectName(u"lbl_systems_dl")
self.formLayout.setWidget(3, QFormLayout.LabelRole, self.lbl_systems_dl)
self.inp_bodies_dl = QComboBox(self.tab_download)
self.inp_bodies_dl.setObjectName(u"inp_bodies_dl")
self.inp_bodies_dl.setEditable(True)
self.inp_bodies_dl.setInsertPolicy(QComboBox.InsertAtTop)
self.formLayout.setWidget(1, QFormLayout.FieldRole, self.inp_bodies_dl)
self.inp_systems_dl = QComboBox(self.tab_download)
self.inp_systems_dl.setObjectName(u"inp_systems_dl")
self.inp_systems_dl.setEditable(True)
self.inp_systems_dl.setInsertPolicy(QComboBox.InsertAtTop)
self.formLayout.setWidget(3, QFormLayout.FieldRole, self.inp_systems_dl)
self.gridLayout = QGridLayout()
self.gridLayout.setObjectName(u"gridLayout")
self.inp_bodies_dest_dl = QComboBox(self.tab_download)
self.inp_bodies_dest_dl.setObjectName(u"inp_bodies_dest_dl")
sizePolicy2 = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
sizePolicy2.setHorizontalStretch(0)
sizePolicy2.setVerticalStretch(0)
sizePolicy2.setHeightForWidth(self.inp_bodies_dest_dl.sizePolicy().hasHeightForWidth())
self.inp_bodies_dest_dl.setSizePolicy(sizePolicy2)
self.inp_bodies_dest_dl.setEditable(False)
self.inp_bodies_dest_dl.setInsertPolicy(QComboBox.InsertAtTop)
self.gridLayout.addWidget(self.inp_bodies_dest_dl, 0, 0, 1, 1)
self.btn_bodies_dest_browse_dl = QPushButton(self.tab_download)
self.btn_bodies_dest_browse_dl.setObjectName(u"btn_bodies_dest_browse_dl")
self.gridLayout.addWidget(self.btn_bodies_dest_browse_dl, 0, 1, 1, 1)
self.formLayout.setLayout(2, QFormLayout.FieldRole, self.gridLayout)
self.gridLayout_2 = QGridLayout()
self.gridLayout_2.setObjectName(u"gridLayout_2")
self.btn_systems_dest_browse_dl = QPushButton(self.tab_download)
self.btn_systems_dest_browse_dl.setObjectName(u"btn_systems_dest_browse_dl")
self.gridLayout_2.addWidget(self.btn_systems_dest_browse_dl, 0, 1, 1, 1)
self.inp_systems_dest_dl = QComboBox(self.tab_download)
self.inp_systems_dest_dl.setObjectName(u"inp_systems_dest_dl")
sizePolicy2.setHeightForWidth(self.inp_systems_dest_dl.sizePolicy().hasHeightForWidth())
self.inp_systems_dest_dl.setSizePolicy(sizePolicy2)
self.inp_systems_dest_dl.setEditable(False)
self.inp_systems_dest_dl.setInsertPolicy(QComboBox.InsertAtTop)
self.gridLayout_2.addWidget(self.inp_systems_dest_dl, 0, 0, 1, 1)
self.formLayout.setLayout(4, QFormLayout.FieldRole, self.gridLayout_2)
self.btn_download = QPushButton(self.tab_download)
self.btn_download.setObjectName(u"btn_download")
self.formLayout.setWidget(5, QFormLayout.LabelRole, self.btn_download)
self.label = QLabel(self.tab_download)
self.label.setObjectName(u"label")
self.formLayout.setWidget(2, QFormLayout.LabelRole, self.label)
self.label_2 = QLabel(self.tab_download)
self.label_2.setObjectName(u"label_2")
self.formLayout.setWidget(4, QFormLayout.LabelRole, self.label_2)
self.tabs.addTab(self.tab_download, "")
self.tab_preprocess = QWidget()
self.tab_preprocess.setObjectName(u"tab_preprocess")
self.formLayout_3 = QFormLayout(self.tab_preprocess)
self.formLayout_3.setObjectName(u"formLayout_3")
self.lbl_bodies_pp = QLabel(self.tab_preprocess)
self.lbl_bodies_pp.setObjectName(u"lbl_bodies_pp")
self.formLayout_3.setWidget(0, QFormLayout.LabelRole, self.lbl_bodies_pp)
self.gr_bodies_pp = QGridLayout()
self.gr_bodies_pp.setObjectName(u"gr_bodies_pp")
self.btn_bodies_browse_pp = QPushButton(self.tab_preprocess)
self.btn_bodies_browse_pp.setObjectName(u"btn_bodies_browse_pp")
self.gr_bodies_pp.addWidget(self.btn_bodies_browse_pp, 0, 1, 1, 1)
self.inp_bodies_pp = QComboBox(self.tab_preprocess)
self.inp_bodies_pp.setObjectName(u"inp_bodies_pp")
sizePolicy2.setHeightForWidth(self.inp_bodies_pp.sizePolicy().hasHeightForWidth())
self.inp_bodies_pp.setSizePolicy(sizePolicy2)
self.inp_bodies_pp.setEditable(False)
self.inp_bodies_pp.setInsertPolicy(QComboBox.InsertAtTop)
self.gr_bodies_pp.addWidget(self.inp_bodies_pp, 0, 0, 1, 1)
self.formLayout_3.setLayout(0, QFormLayout.FieldRole, self.gr_bodies_pp)
self.lbl_systems_pp = QLabel(self.tab_preprocess)
self.lbl_systems_pp.setObjectName(u"lbl_systems_pp")
self.formLayout_3.setWidget(1, QFormLayout.LabelRole, self.lbl_systems_pp)
self.gr_systems_pp = QGridLayout()
self.gr_systems_pp.setObjectName(u"gr_systems_pp")
self.btn_systems_browse_pp = QPushButton(self.tab_preprocess)
self.btn_systems_browse_pp.setObjectName(u"btn_systems_browse_pp")
self.gr_systems_pp.addWidget(self.btn_systems_browse_pp, 0, 1, 1, 1)
self.inp_systems_pp = QComboBox(self.tab_preprocess)
self.inp_systems_pp.setObjectName(u"inp_systems_pp")
sizePolicy2.setHeightForWidth(self.inp_systems_pp.sizePolicy().hasHeightForWidth())
self.inp_systems_pp.setSizePolicy(sizePolicy2)
self.inp_systems_pp.setEditable(False)
self.inp_systems_pp.setInsertPolicy(QComboBox.InsertAtTop)
self.gr_systems_pp.addWidget(self.inp_systems_pp, 0, 0, 1, 1)
self.formLayout_3.setLayout(1, QFormLayout.FieldRole, self.gr_systems_pp)
self.lbl_out_pp = QLabel(self.tab_preprocess)
self.lbl_out_pp.setObjectName(u"lbl_out_pp")
self.formLayout_3.setWidget(2, QFormLayout.LabelRole, self.lbl_out_pp)
self.gr_out_grid_pp = QGridLayout()
self.gr_out_grid_pp.setObjectName(u"gr_out_grid_pp")
self.btn_out_browse_pp = QPushButton(self.tab_preprocess)
self.btn_out_browse_pp.setObjectName(u"btn_out_browse_pp")
sizePolicy3 = QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed)
sizePolicy3.setHorizontalStretch(0)
sizePolicy3.setVerticalStretch(0)
sizePolicy3.setHeightForWidth(self.btn_out_browse_pp.sizePolicy().hasHeightForWidth())
self.btn_out_browse_pp.setSizePolicy(sizePolicy3)
self.gr_out_grid_pp.addWidget(self.btn_out_browse_pp, 0, 1, 1, 1)
self.inp_out_pp = QComboBox(self.tab_preprocess)
self.inp_out_pp.setObjectName(u"inp_out_pp")
sizePolicy2.setHeightForWidth(self.inp_out_pp.sizePolicy().hasHeightForWidth())
self.inp_out_pp.setSizePolicy(sizePolicy2)
self.inp_out_pp.setEditable(False)
self.inp_out_pp.setInsertPolicy(QComboBox.InsertAtTop)
self.gr_out_grid_pp.addWidget(self.inp_out_pp, 0, 0, 1, 1)
self.formLayout_3.setLayout(2, QFormLayout.FieldRole, self.gr_out_grid_pp)
self.btn_preprocess = QPushButton(self.tab_preprocess)
self.btn_preprocess.setObjectName(u"btn_preprocess")
self.formLayout_3.setWidget(3, QFormLayout.LabelRole, self.btn_preprocess)
self.tabs.addTab(self.tab_preprocess, "")
self.tab_route = QWidget()
self.tab_route.setObjectName(u"tab_route")
self.formLayout_2 = QFormLayout(self.tab_route)
self.formLayout_2.setObjectName(u"formLayout_2")
self.lbl_sys_lst = QLabel(self.tab_route)
self.lbl_sys_lst.setObjectName(u"lbl_sys_lst")
self.formLayout_2.setWidget(0, QFormLayout.LabelRole, self.lbl_sys_lst)
self.gr_sys = QGridLayout()
self.gr_sys.setObjectName(u"gr_sys")
self.btn_sys_lst_browse = QPushButton(self.tab_route)
self.btn_sys_lst_browse.setObjectName(u"btn_sys_lst_browse")
self.gr_sys.addWidget(self.btn_sys_lst_browse, 0, 1, 1, 1)
self.inp_sys_lst = QComboBox(self.tab_route)
self.inp_sys_lst.setObjectName(u"inp_sys_lst")
sizePolicy2.setHeightForWidth(self.inp_sys_lst.sizePolicy().hasHeightForWidth())
self.inp_sys_lst.setSizePolicy(sizePolicy2)
self.inp_sys_lst.setEditable(False)
self.inp_sys_lst.setInsertPolicy(QComboBox.InsertAtTop)
self.inp_sys_lst.setFrame(True)
self.inp_sys_lst.setModelColumn(0)
self.gr_sys.addWidget(self.inp_sys_lst, 0, 0, 1, 1)
self.formLayout_2.setLayout(0, QFormLayout.FieldRole, self.gr_sys)
self.btn_add = QPushButton(self.tab_route)
self.btn_add.setObjectName(u"btn_add")
self.formLayout_2.setWidget(1, QFormLayout.LabelRole, self.btn_add)
self.inp_sys = QLineEdit(self.tab_route)
self.inp_sys.setObjectName(u"inp_sys")
self.formLayout_2.setWidget(1, QFormLayout.FieldRole, self.inp_sys)
self.btn_rm = QPushButton(self.tab_route)
self.btn_rm.setObjectName(u"btn_rm")
self.formLayout_2.setWidget(3, QFormLayout.LabelRole, self.btn_rm)
self.gr_mode = QGridLayout()
self.gr_mode.setObjectName(u"gr_mode")
self.rd_comp = QRadioButton(self.tab_route)
self.rd_comp.setObjectName(u"rd_comp")
self.rd_comp.setChecked(True)
self.gr_mode.addWidget(self.rd_comp, 0, 1, 1, 1)
self.rd_precomp = QRadioButton(self.tab_route)
self.rd_precomp.setObjectName(u"rd_precomp")
self.gr_mode.addWidget(self.rd_precomp, 0, 2, 1, 1)
self.formLayout_2.setLayout(3, QFormLayout.FieldRole, self.gr_mode)
self.chk_permute = QCheckBox(self.tab_route)
self.chk_permute.setObjectName(u"chk_permute")
self.formLayout_2.setWidget(4, QFormLayout.LabelRole, self.chk_permute)
self.gridLayout_4 = QGridLayout()
self.gridLayout_4.setObjectName(u"gridLayout_4")
self.chk_permute_keep_last = QCheckBox(self.tab_route)
self.chk_permute_keep_last.setObjectName(u"chk_permute_keep_last")
self.gridLayout_4.addWidget(self.chk_permute_keep_last, 0, 3, 1, 1)
self.chk_permute_keep_first = QCheckBox(self.tab_route)
self.chk_permute_keep_first.setObjectName(u"chk_permute_keep_first")
sizePolicy3.setHeightForWidth(self.chk_permute_keep_first.sizePolicy().hasHeightForWidth())
self.chk_permute_keep_first.setSizePolicy(sizePolicy3)
self.chk_permute_keep_first.setTristate(False)
self.gridLayout_4.addWidget(self.chk_permute_keep_first, 0, 2, 1, 1)
self.lbl_keep = QLabel(self.tab_route)
self.lbl_keep.setObjectName(u"lbl_keep")
sizePolicy1.setHeightForWidth(self.lbl_keep.sizePolicy().hasHeightForWidth())
self.lbl_keep.setSizePolicy(sizePolicy1)
self.gridLayout_4.addWidget(self.lbl_keep, 0, 1, 1, 1)
self.formLayout_2.setLayout(4, QFormLayout.FieldRole, self.gridLayout_4)
self.lst_sys = QTreeWidget(self.tab_route)
__qtreewidgetitem = QTreeWidgetItem()
__qtreewidgetitem.setText(0, u"Name");
self.lst_sys.setHeaderItem(__qtreewidgetitem)
self.lst_sys.setObjectName(u"lst_sys")
sizePolicy4 = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
sizePolicy4.setHorizontalStretch(0)
sizePolicy4.setVerticalStretch(0)
sizePolicy4.setHeightForWidth(self.lst_sys.sizePolicy().hasHeightForWidth())
self.lst_sys.setSizePolicy(sizePolicy4)
self.lst_sys.setMinimumSize(QSize(0, 0))
self.lst_sys.setDragEnabled(True)
self.lst_sys.setDragDropOverwriteMode(False)
self.lst_sys.setDragDropMode(QAbstractItemView.InternalMove)
self.lst_sys.setDefaultDropAction(Qt.MoveAction)
self.lst_sys.setAlternatingRowColors(True)
self.lst_sys.setSelectionMode(QAbstractItemView.ExtendedSelection)
self.lst_sys.setSelectionBehavior(QAbstractItemView.SelectRows)
self.lst_sys.setHeaderHidden(True)
self.lst_sys.header().setVisible(False)
self.formLayout_2.setWidget(7, QFormLayout.SpanningRole, self.lst_sys)
self.lbl_range = QLabel(self.tab_route)
self.lbl_range.setObjectName(u"lbl_range")
self.formLayout_2.setWidget(8, QFormLayout.LabelRole, self.lbl_range)
self.sb_range = QDoubleSpinBox(self.tab_route)
self.sb_range.setObjectName(u"sb_range")
self.formLayout_2.setWidget(8, QFormLayout.FieldRole, self.sb_range)
self.gr_opts = QGridLayout()
self.gr_opts.setObjectName(u"gr_opts")
self.cmb_mode = QComboBox(self.tab_route)
self.cmb_mode.addItem("")
self.cmb_mode.addItem("")
self.cmb_mode.addItem("")
self.cmb_mode.setObjectName(u"cmb_mode")
self.gr_opts.addWidget(self.cmb_mode, 0, 2, 1, 1)
self.lbl_greedyness = QLabel(self.tab_route)
self.lbl_greedyness.setObjectName(u"lbl_greedyness")
self.lbl_greedyness.setEnabled(True)
self.gr_opts.addWidget(self.lbl_greedyness, 1, 1, 1, 1)
self.chk_primary = QCheckBox(self.tab_route)
self.chk_primary.setObjectName(u"chk_primary")
self.gr_opts.addWidget(self.chk_primary, 0, 3, 1, 1)
self.sld_greedyness = QSlider(self.tab_route)
self.sld_greedyness.setObjectName(u"sld_greedyness")
self.sld_greedyness.setMaximum(100)
self.sld_greedyness.setPageStep(10)
self.sld_greedyness.setValue(50)
self.sld_greedyness.setOrientation(Qt.Horizontal)
self.sld_greedyness.setTickPosition(QSlider.TicksBelow)
self.sld_greedyness.setTickInterval(10)
self.gr_opts.addWidget(self.sld_greedyness, 1, 2, 1, 2)
self.lbl_mode = QLabel(self.tab_route)
self.lbl_mode.setObjectName(u"lbl_mode")
self.gr_opts.addWidget(self.lbl_mode, 0, 1, 1, 1)
self.formLayout_2.setLayout(9, QFormLayout.SpanningRole, self.gr_opts)
self.btn_go = QPushButton(self.tab_route)
self.btn_go.setObjectName(u"btn_go")
self.btn_go.setFlat(False)
self.formLayout_2.setWidget(10, QFormLayout.LabelRole, self.btn_go)
self.tabs.addTab(self.tab_route, "")
self.tab_log = QWidget()
self.tab_log.setObjectName(u"tab_log")
self.gridLayout_3 = QGridLayout(self.tab_log)
self.gridLayout_3.setObjectName(u"gridLayout_3")
self.txt_log = QTextEdit(self.tab_log)
self.txt_log.setObjectName(u"txt_log")
self.txt_log.setEnabled(True)
self.txt_log.setFrameShadow(QFrame.Sunken)
self.txt_log.setLineWidth(1)
self.txt_log.setReadOnly(True)
self.txt_log.setAcceptRichText(False)
self.gridLayout_3.addWidget(self.txt_log, 0, 0, 1, 1)
self.tabs.addTab(self.tab_log, "")
self.verticalLayout.addWidget(self.tabs)
ED_LRR.setCentralWidget(self.centralwidget)
self.menu = QMenuBar(ED_LRR)
self.menu.setObjectName(u"menu")
self.menu.setGeometry(QRect(0, 0, 577, 21))
self.menu_file = QMenu(self.menu)
self.menu_file.setObjectName(u"menu_file")
self.menuWindow = QMenu(self.menu)
self.menuWindow.setObjectName(u"menuWindow")
self.menuStyle = QMenu(self.menuWindow)
self.menuStyle.setObjectName(u"menuStyle")
ED_LRR.setMenuBar(self.menu)
self.bar_status = QStatusBar(ED_LRR)
self.bar_status.setObjectName(u"bar_status")
ED_LRR.setStatusBar(self.bar_status)
QWidget.setTabOrder(self.rd_comp, self.cmb_mode)
QWidget.setTabOrder(self.cmb_mode, self.chk_primary)
QWidget.setTabOrder(self.chk_primary, self.sld_greedyness)
QWidget.setTabOrder(self.sld_greedyness, self.rd_precomp)
self.menu.addAction(self.menu_file.menuAction())
self.menu.addAction(self.menuWindow.menuAction())
self.menu_file.addAction(self.menu_act_quit)
self.menuWindow.addAction(self.menuStyle.menuAction())
self.retranslateUi(ED_LRR)
self.menu_act_quit.triggered.connect(ED_LRR.close)
self.tabs.setCurrentIndex(2)
QMetaObject.connectSlotsByName(ED_LRR)
# setupUi
def retranslateUi(self, ED_LRR):
ED_LRR.setWindowTitle(QCoreApplication.translate("ED_LRR", u"Elite: Dangerous Long Range Route Plotter", None))
self.menu_act_quit.setText(QCoreApplication.translate("ED_LRR", u"Quit", None))
#if QT_CONFIG(shortcut)
self.menu_act_quit.setShortcut(QCoreApplication.translate("ED_LRR", u"Ctrl+Q", None))
#endif // QT_CONFIG(shortcut)
self.actionA.setText(QCoreApplication.translate("ED_LRR", u"A", None))
self.actionB.setText(QCoreApplication.translate("ED_LRR", u"B", None))
self.lbl_bodies_dl.setText(QCoreApplication.translate("ED_LRR", u"bodies.json", None))
self.lbl_systems_dl.setText(QCoreApplication.translate("ED_LRR", u"systemsWithCoordinates.json", None))
self.inp_bodies_dl.setCurrentText(QCoreApplication.translate("ED_LRR", u"https://www.edsm.net/dump/bodies.json", None))
self.inp_systems_dl.setCurrentText(QCoreApplication.translate("ED_LRR", u"https://www.edsm.net/dump/systemsWithCoordinates.json", None))
self.btn_bodies_dest_browse_dl.setText(QCoreApplication.translate("ED_LRR", u"...", None))
self.btn_systems_dest_browse_dl.setText(QCoreApplication.translate("ED_LRR", u"...", None))
self.btn_download.setText(QCoreApplication.translate("ED_LRR", u"Download", None))
self.label.setText(QCoreApplication.translate("ED_LRR", u"Download path", None))
self.label_2.setText(QCoreApplication.translate("ED_LRR", u"Download path", None))
self.tabs.setTabText(self.tabs.indexOf(self.tab_download), QCoreApplication.translate("ED_LRR", u"Download", None))
self.lbl_bodies_pp.setText(QCoreApplication.translate("ED_LRR", u"bodies.json", None))
self.btn_bodies_browse_pp.setText(QCoreApplication.translate("ED_LRR", u"...", None))
self.lbl_systems_pp.setText(QCoreApplication.translate("ED_LRR", u"systemsWithCoordinates.json", None))
self.btn_systems_browse_pp.setText(QCoreApplication.translate("ED_LRR", u"...", None))
self.lbl_out_pp.setText(QCoreApplication.translate("ED_LRR", u"Output", None))
self.btn_out_browse_pp.setText(QCoreApplication.translate("ED_LRR", u"...", None))
self.btn_preprocess.setText(QCoreApplication.translate("ED_LRR", u"Preprocess", None))
self.tabs.setTabText(self.tabs.indexOf(self.tab_preprocess), QCoreApplication.translate("ED_LRR", u"Preprocess", None))
self.lbl_sys_lst.setText(QCoreApplication.translate("ED_LRR", u"System List", None))
self.btn_sys_lst_browse.setText(QCoreApplication.translate("ED_LRR", u"...", None))
self.btn_add.setText(QCoreApplication.translate("ED_LRR", u"Add", None))
self.inp_sys.setPlaceholderText(QCoreApplication.translate("ED_LRR", u"System Name", None))
self.btn_rm.setText(QCoreApplication.translate("ED_LRR", u"Remove", None))
self.rd_comp.setText(QCoreApplication.translate("ED_LRR", u"Compute Route", None))
self.rd_precomp.setText(QCoreApplication.translate("ED_LRR", u"Precompute Graph", None))
self.chk_permute.setText(QCoreApplication.translate("ED_LRR", u"Permute", None))
self.chk_permute_keep_last.setText(QCoreApplication.translate("ED_LRR", u"Last", None))
self.chk_permute_keep_first.setText(QCoreApplication.translate("ED_LRR", u"First", None))
self.lbl_keep.setText(QCoreApplication.translate("ED_LRR", u"Keep Endpoints:", None))
___qtreewidgetitem = self.lst_sys.headerItem()
___qtreewidgetitem.setText(1, QCoreApplication.translate("ED_LRR", u"Type", None));
self.lbl_range.setText(QCoreApplication.translate("ED_LRR", u"Jump Range (Ly)", None))
self.cmb_mode.setItemText(0, QCoreApplication.translate("ED_LRR", u"Breadth-First Search", None))
self.cmb_mode.setItemText(1, QCoreApplication.translate("ED_LRR", u"Greedy-Search", None))
self.cmb_mode.setItemText(2, QCoreApplication.translate("ED_LRR", u"A*-Search", None))
self.cmb_mode.setCurrentText(QCoreApplication.translate("ED_LRR", u"Breadth-First Search", None))
self.lbl_greedyness.setText(QCoreApplication.translate("ED_LRR", u"Greedyness Factor", None))
self.chk_primary.setText(QCoreApplication.translate("ED_LRR", u"Primary Stars Only", None))
self.lbl_mode.setText(QCoreApplication.translate("ED_LRR", u"Mode", None))
self.btn_go.setText(QCoreApplication.translate("ED_LRR", u"GO!", None))
self.tabs.setTabText(self.tabs.indexOf(self.tab_route), QCoreApplication.translate("ED_LRR", u"Route", None))
self.tabs.setTabText(self.tabs.indexOf(self.tab_log), QCoreApplication.translate("ED_LRR", u"Log", None))
self.menu_file.setTitle(QCoreApplication.translate("ED_LRR", u"File", None))
self.menuWindow.setTitle(QCoreApplication.translate("ED_LRR", u"Window", None))
self.menuStyle.setTitle(QCoreApplication.translate("ED_LRR", u"Style", None))
# retranslateUi

View File

@ -0,0 +1,79 @@
# -*- coding: utf-8 -*-
################################################################################
## Form generated from reading UI file 'widget_route.ui'
##
## Created by: Qt User Interface Compiler version 5.15.2
##
## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################
from PySide2.QtCore import *
from PySide2.QtGui import *
from PySide2.QtWidgets import *
class Ui_diag_route(object):
def setupUi(self, diag_route):
if not diag_route.objectName():
diag_route.setObjectName(u"diag_route")
diag_route.setWindowModality(Qt.WindowModal)
diag_route.resize(430, 300)
self.layout_main = QGridLayout(diag_route)
self.layout_main.setObjectName(u"layout_main")
self.layout_top = QGridLayout()
self.layout_top.setObjectName(u"layout_top")
self.lst_route = QTreeWidget(diag_route)
self.lst_route.setObjectName(u"lst_route")
self.lst_route.setProperty("showDropIndicator", False)
self.lst_route.setDefaultDropAction(Qt.IgnoreAction)
self.lst_route.setAlternatingRowColors(True)
self.lst_route.setSelectionMode(QAbstractItemView.NoSelection)
self.lst_route.setVerticalScrollMode(QAbstractItemView.ScrollPerItem)
self.lst_route.setHorizontalScrollMode(QAbstractItemView.ScrollPerPixel)
self.lst_route.setItemsExpandable(False)
self.lst_route.setAllColumnsShowFocus(False)
self.layout_top.addWidget(self.lst_route, 0, 0, 1, 1)
self.layout_main.addLayout(self.layout_top, 0, 0, 1, 1)
self.layout_bottom = QGridLayout()
self.layout_bottom.setObjectName(u"layout_bottom")
self.chk_copy = QCheckBox(diag_route)
self.chk_copy.setObjectName(u"chk_copy")
self.layout_bottom.addWidget(self.chk_copy, 1, 0, 1, 1)
self.btn_close = QPushButton(diag_route)
self.btn_close.setObjectName(u"btn_close")
self.layout_bottom.addWidget(self.btn_close, 1, 2, 1, 1)
self.btn_export = QPushButton(diag_route)
self.btn_export.setObjectName(u"btn_export")
self.layout_bottom.addWidget(self.btn_export, 1, 1, 1, 1)
self.layout_main.addLayout(self.layout_bottom, 1, 0, 1, 1)
self.retranslateUi(diag_route)
QMetaObject.connectSlotsByName(diag_route)
# setupUi
def retranslateUi(self, diag_route):
diag_route.setWindowTitle(QCoreApplication.translate("diag_route", u"Route", None))
___qtreewidgetitem = self.lst_route.headerItem()
___qtreewidgetitem.setText(3, QCoreApplication.translate("diag_route", u"Distance (Ls)", None));
___qtreewidgetitem.setText(2, QCoreApplication.translate("diag_route", u"Body", None));
___qtreewidgetitem.setText(1, QCoreApplication.translate("diag_route", u"System", None));
___qtreewidgetitem.setText(0, QCoreApplication.translate("diag_route", u"Num", None));
self.chk_copy.setText(QCoreApplication.translate("diag_route", u"Auto-copy next hop to clipboard", None))
self.btn_close.setText(QCoreApplication.translate("diag_route", u"Close", None))
self.btn_export.setText(QCoreApplication.translate("diag_route", u"Export", None))
# retranslateUi

View File

@ -108,46 +108,3 @@ $$dist = \frac{boost \cdot m_{opt} \cdot \left(1.0 \cdot \left(\frac{boost^{2} \
$$dist = \frac{- B_{g} \cdot m_{fuel} - B_{g} \cdot m_{ship} + boost \cdot m_{opt} \cdot \left(\frac{1000.0 \cdot e_{fuel} \cdot \min\left(f_{max}, m_{fuel}\right)}{l}\right)^{\frac{1}{p}}}{m_{fuel} + m_{ship}}$$
$$e_{fuel} = \frac{l \cdot \left(\frac{10.0^{- \frac{3.0}{p}} \cdot \left(B_{g} + dist\right) \cdot \left(m_{fuel} + m_{ship}\right)}{boost \cdot m_{opt}}\right)^{p}}{\min\left(f_{max}, m_{fuel}\right)}$$
---
# TODO
## Routing
- Implement Neutron Mode
- Filter for neutron stars, plot route, then plot "fine" router between waypoints
- What Jump-Range to use for neutron route? `max_range*4`?
- Implement Bidir BFS
- Optimized All-Pairs BFS for graph precomputation
- Take fuel consumption into account (WIP)
- Guardian Booster support (Done?)
- Economic routing
- Custom weights and filtering for routing
## GUI
- Implement estimate time to completion display for route computation and preprocessing (Done?)
- Export route as:
- JSON
- HTML (WIP)
- CSV
- SVG
## Installer
- Update PATH from installer
## Preprocessing
- Build index over `systemsWithCoordinates.json` instead of loading it into RAM (reuse modified `LineCache` from `router.rs`)
- Finish `galaxy.jsonl` preprocessor
- Implement Python interface to preprocessor
## Misc
- Luigi based Task queue for distributed routing
- Full route tree computation
- overlap elimination

49
TODO.md Normal file
View File

@ -0,0 +1,49 @@
# TODO
## API
- expose multiroute TSP optimizer
## Routing
- beam stack search, queue for each depth level, ordered by distance to goal node (min first)
- GraphSearch trait
- Implement Neutron Mode
- Filter for neutron stars, plot coarse route, then plot exact router between waypoints
- What Jump-Range to use for neutron route? `max_range*4`?
- furthest inside jump range, otherwise closest?
- Implement Bidirectional BFS
- Optimized All-Pairs BFS for graph precomputation
- Take fuel consumption into account (WIP) partially implemented
- Guardian Booster support (Done?)
- Economic routing (minimal fuel, dijkstra)
- implemented, needs to be properly exposed
- Custom weights and filtering for routing [evalexpr](https://docs.rs/evalexpr/)
- pathfinding weighted by $(goal-pos)\cdot(goal-start)$
- use vecmat crate for vector distance etc
## GUI
- Imgui?
- Implement estimate time to completion display for route computation and preprocessing (Done?)
- Export route as:
- JSON
- HTML (WIP)
- CSV
- SVG
## Installer
- Update PATH from installer
## Preprocessing
- Build index over `systemsWithCoordinates.json` instead of loading it into RAM (reuse modified `LineCache` from `router.rs`)
- Finish `galaxy.jsonl` preprocessor (Done?)
- Implement Python interface to preprocessor
## Misc
- Luigi/Celery/Dask based Task queue for distributed routing

6
beam_stack_impl.py Normal file
View File

@ -0,0 +1,6 @@
def pruntLayer(l,w):
Keep=sorted(Open[l])[:w] # best `w` node from `Open[l]`
Prune = [n for n in Open[l] if n not in Keep]
beam_stack.top().f_max=min([f(n) for n in Prune])
for n in Prune:
Open[l].remove(n)

84
benchmark_sweep.py Normal file
View File

@ -0,0 +1,84 @@
from tqdm import tqdm
import time
import json
import os
import statistics
def stats(values):
ret={
"max":max(values),
"min":min(values),
"mean":statistics.mean(values),
"range":abs(max(values)-min(values)),
"sum":sum(values)
}
if len(values)>1:
ret["stdev"]=statistics.stdev(values)
else:
ret["stdev"]=0
return ret
last_seen=float('inf')
pbar=None
def callback(state):
try:
global pbar,last_seen
if state['n_seen']<last_seen:
if pbar:
pbar.close()
pbar=tqdm(total=state['d_total'],unit="Ly",unit_scale=True)
last_seen=state['n_seen']
pbar.n=round(state['d_total']-state['d_rem'],2)
pbar.set_description("[J:{depth} | Q:{queue_size} | D:{d_rem:.2f} Ly | S:{n_seen} ({prc_seen:.2f}%)] {system}".format(**state))
pbar.update(0)
except Exception as e:
print(e)
if not 'callback' in vars():
callback=None
import _ed_lrr
callback=None
r=_ed_lrr.PyRouter(callback)
r.load("stars.csv")
res=[]
done=set()
if os.path.isfile("res.json"):
with open("res.json","r") as fh:
try:
res=json.load(fh)
except:
pass
for v in res:
done.add((v[0],v[1]))
systems=r.resolve_systems("Sol","Colonia")
min_dt_total=60*10
print("Warming up...")
r.route([systems['Sol'],systems['Sol']],48.0,0.0,0.0,0) # warmup
print("Done")
for g in [0.0, 0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9,1.0]:
for w in [0.0, 0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9,1.0]:
if (g,w) in done:
continue
dt=[]
lr=[]
l=0
while sum(dt)<min_dt_total:
l+=1
print({'g':g,'w':w,'l':l})
t=time.time()
route=r.route([systems['Sol'],systems['Colonia']],48.0,g,w,0)
dt.append(time.time()-t)
lr.append(len(route))
print(stats(dt))
dt=sum(dt)/len(dt)
res.append((g,w,lr,dt))
with open("res.json","w") as of:
json.dump(res,of)
# for n in range(21):
# grid=r.get_grid(1<<n)
# max_v=max(grid.items(),key=lambda v:v[1])
# mean_v=sum(grid.values())/len(grid)
# print(1<<n,len(grid),mean_v,max_v)

View File

@ -3,9 +3,8 @@ from celery import Celery
import _ed_lrr
import os
os.environ.setdefault("CELERY_CONFIG_MODULE", "celeryconfig")
app = Celery("ed_lrr")
app.config_from_envvar("CELERY_CONFIG_MODULE")
app.config_from_object(__import__("celeryconfig"))
@app.task(bind=True)

View File

@ -1,48 +0,0 @@
MD = $(wildcard src/*.md)
DOTS = $(wildcard src/*.dot)
ASYS = $(wildcard src/*.asy)
PYS = $(wildcard src/img_*.py)
PDFS = $(MD:src/%.md=out/%.pdf)
IMG_PDFS = $(ASYS:src/%.asy=img/%.pdf) $(PYS:src/img_%.py=img/%.pdf) $(DOTS:src/%.dot=img/%.pdf)
IMGS = $(IMG_PDFS)
TEMPLATE = eisvogel
PDF_ENGINE = xelatex
PANDOC = pandoc
PANDOC_OPTIONS = -F panflute -F pandoc-citeproc --pdf-engine=$(PDF_ENGINE) --template $(TEMPLATE) -N --standalone --listings
GRAPHVIZ = dot
GRAPHVIZ_OPTIONS = -Tpdf
ASY = asy
ASY_OPTIONS = -noV -f pdf
PYTHON = python
PYTHON_OPTIONS =
mkfile_path := $(abspath $(lastword $(MAKEFILE_LIST)))
current_dir := $(notdir $(patsubst %/,%,$(dir $(mkfile_path))))
.PHONY: clean all default
all: $(PDFS)
default: all
out/%.pdf: src/%.md $(IMGS) Makefile
$(PANDOC) $(PANDOC_OPTIONS) -o $@ $<
img/%.pdf: src/%.dot
$(GRAPHVIZ) $(GRAPHVIZ_OPTIONS) -o $@ $<
img/%.pdf: src/img_%.py
$(PYTHON) $(PYTHON_OPTIONS) $< $@
img/%.pdf: src/%.asy
$(ASY) $(ASY_OPTIONS) -o $@ $<
watch:
watchexec -w src -w data -w filters -w Makefile make all
clean:
-rm $(PDFS) $(IMGS)

View File

@ -1,158 +0,0 @@
# -*- coding: utf-8 -*-
import contextlib
import csv
import datetime
import hashlib
import io
import os
import re
import subprocess as SP
import sys
import tempfile
from functools import partial
import panflute as pf
from dateutil.parser import parse as dateparse
from jinja2 import Environment, PackageLoader, Template, select_autoescape
from panflute import *
def remove_pound(elem, doc):
if type(elem) == Str:
return Str(elem.text.lstrip("#"))
def fix_color(elem, doc):
if type(elem) == MetaMap:
for k in elem.content:
if k.endswith("-color"):
elem[k] = elem[k].walk(remove_pound)
def update_date(elem, doc):
if type(elem) == MetaMap:
datefmt = doc.get_metadata("datefmt", "%Y-%m-%d")
today = datetime.date.today().strftime(datefmt)
date = dateparse(doc.get_metadata("date", today)).date()
elem["date"] = MetaInlines(Str(date.strftime(datefmt)))
return elem
def csv_table(elem, doc):
if type(elem) == Para and len(elem.content) == 1 and type(elem.content[0]) == Image:
elem = elem.content[0]
ext = os.path.splitext(elem.url)[1][1:]
if ext == "csv":
caption = elem.content
has_header = elem.attributes.get("has-header", "false").lower() == "true"
with open(elem.url) as f:
reader = csv.reader(f)
body = []
for row in reader:
cells = [TableCell(Plain(Str(x))) for x in row]
body.append(TableRow(*cells))
header = body.pop(0) if has_header else None
ret = Table(*body, header=header, caption=caption)
return ret
def code_refs(elem, doc):
if type(elem) == Cite:
label = elem.content[0]
if type(label) == Str:
label = label.text
filename = re.findall(r"^\[@lst:(.*)\]$", label) or [None]
if filename[0] in doc.inc_files:
return [
RawInline(
"\\hyperref[{}]{{{}}}".format(filename[0], filename[0]),
format="tex",
)
]
def include_code(elem, doc):
if type(elem) == CodeBlock:
if "include" in elem.attributes:
filepath = elem.attributes.pop("include")
filename = os.path.split(filepath)[-1]
try:
elem.text += elem.text + open(filepath, encoding="utf-8").read()
elem.attributes["caption"] = filename
doc.inc_files.append(filename)
except Exception as e:
elem.text += "Error: {}".format(e)
return [RawBlock("\\label{{{}}}".format(filename), format="tex"), elem]
def py_eval(options, data, element, doc):
out_buffer = io.StringIO()
with contextlib.redirect_stdout(out_buffer):
exec(data, doc.pyenv)
out_buffer.seek(0)
return convert_text(out_buffer.read())
def jinja_py_filt(doc, file):
env = {}
code = open(file, encoding="utf-8").read()
exec(code, env)
return env["main"](doc)
def prepare(doc):
doc.inc_files = []
doc.env = Environment()
doc.pyenv = {}
filters = {"py": partial(jinja_py_filt, doc)}
doc.env.filters.update(filters)
def process_templates(elem, doc):
if type(elem) == CodeBlock:
if elem.classes == ["@"]:
args = {"meta": doc.get_metadata()}
return convert_text(doc.env.from_string(elem.text).render(args))
def yaml_filt(elem, doc):
tags = {"eval": py_eval}
return yaml_filter(elem, doc, tags=tags, strict_yaml=True)
def checkboxes(elem, doc):
if type(elem) in [Para, Plain]:
val = re.findall(r"^\[([xX]|\ )\] (.*)$", stringify(elem))
if val:
val = val[0][0].lower() == "x"
else:
return elem
marker = {
True: RawInline("$\\boxtimes$", format="latex"),
False: RawInline("$\\square$", format="latex"),
}[val]
cont = []
if val:
cont += elem.content[2:]
else:
cont += elem.content[4:]
return Plain(marker, Space, *cont)
return elem
def main(doc=None):
f = [
process_templates,
update_date,
csv_table,
include_code,
fix_color,
code_refs,
yaml_filt,
checkboxes,
]
return run_filters(f, prepare=prepare, doc=doc)
if __name__ == "__main__":
main()

View File

@ -1,92 +0,0 @@
---
# Metadata
title: ED_LRR
author:
- Daniel Seiller <earthnuker@gmail.com>
subtitle: 'Elite Dangerous: Long-Range Router'
# Formating
toc: true
lang: en
colorlinks: true
papersize: a4
numbersections: true
#Panflute options
panflute-filters: [multifilter]
panflute-path: 'filters'
#Template options
titlepage: true
toc-own-page: false
---
\pagebreak
# Implementation
## `stars.csv` format
### Columns
| Name | Content |
| --------- | ------------------------------------------------------------ |
| id | unique ID-Number (not equal to id or id64, just a sequential number) |
| star_type | Type of Star |
| system | Name of System |
| body | Name of Star |
| mult | Jump Range Multiplier (1.5 for White Dwarfs, 4.0 for Neutron Stars) |
| distance | Distance from arrival in Ls |
| x,y,z | Position in Galactic Coordinates with Sol at (0,0,0) |
## `stars.idx` format
`bincode` serialized data:
- **[u64]**: List of byte offset for records (entry 0=first recod, entry 1= second record, etc)
## Routing Algorithms
### Breadth-First Search (BFS)
Standard Breadth-First Search, always finds the shortest route
### A*-Search
Modified A*-Search with adjustable "greediness". Priority Queue weighted by $\text{number of jumps from start system} + (\text{estimated number of jumps to target system} * \text{greediness})$
A greediness of 0 is equivalent to BFS and a greediness of $\infty$ is equivalent to Greedy-Search
### Greedy-Search
Priority Queue weighted by minimum distance to target, prefers systems with high multiplier (Neutron Stars and White Dwarfs)
## Optimizations
### Ellipse elimination
Only consider systems within an ellipsoid with source and destination as the foci, the width of the ellipsoid is adjustable
## Routing Graphs
### File format
`bincode` serialized data:
- *bool* **primary**: flag to indicate that graph only includes primary stars
- *f32* **range**: jump range for routing graph
- *[u8]* **file_hash**: sha3 hash of `stars.csv` from which graph was generated
- *String* **path**: path to `stars.csv` from which graph was generated
- *FnvHashMap* **graph**: Hashmap mapping systems to the systems from which they can be rached, traversed from destination system backwards to source to reconstruct route
# Usage
<!--
TODO: Add screenshots
-->
## Preprocessing Data
## Plotting a Route
# [Changelog](https://gitlab.com/Earthnuker/ed_lrr/blob/pyqt_gui/CHANGELOG.md)

View File

@ -1,83 +0,0 @@
# -*- coding: utf-8 -*-
import heapq
import sys
import numpy as np
import pylab as PL
from scipy.spatial.ckdtree import cKDTree
exit()
def vec(a, b):
return b - a
def bfs(points):
return
def in_ellipse(p, f1, f2, r, offset=0):
df = ((f1 - f2) ** 2).sum(0) ** 0.5
d_f1 = ((p - f1) ** 2).sum(1) ** 0.5
d_f2 = ((p - f2) ** 2).sum(1) ** 0.5
return (d_f1 + d_f2) < (df * (1 + r))
num_points = 100000
p_orig = np.random.normal(0, 10, size=(num_points, 2))
tree = cKDTree(p_orig)
f1 = np.array([0, -30])
f2 = -f1 # np.random.normal(0, 20, (3,))
# r = 2 ** ((n / cnt) - cnt)
mask = in_ellipse(p_orig, f1, f2, 0.1)
p = p_orig[mask]
p_orig = p_orig[~mask]
colors = np.random.random(p.shape[0])
fig = PL.gcf()
PL.scatter(
p_orig[:, 0],
p_orig[:, 1],
marker=".",
s=0.2,
edgecolor="None",
c=[(0.0, 0.0, 0.0)],
alpha=0.75,
rasterized=True,
)
PL.scatter(
p[:, 0],
p[:, 1],
marker="s",
s=0.2,
edgecolor="None",
c=colors,
rasterized=True
)
PL.plot(f1[0], f1[1], "r.", label="Source")
PL.plot(f2[0], f2[1], "g.", label="Destination")
max_v = max(
p_orig[:, 0].max(),
p_orig[:, 1].max(),
f1[0], f1[1],
f2[0], f2[1]
) + 2
min_v = min(
p_orig[:, 0].min(),
p_orig[:, 1].min(),
f1[0], f1[1],
f2[0], f2[1]
) - 2
PL.xlim(min_v, max_v)
PL.ylim(min_v, max_v)
PL.legend()
PL.savefig(sys.argv[1], dpi=1200)

1
docs_mdbook/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
book

View File

@ -4,8 +4,8 @@
"en"
],
"spellright.documentTypes": [
"markdown",
"latex",
"plaintext"
"plaintext",
"git-commit"
]
}
}

View File

@ -0,0 +1,6 @@
{
"todo": [],
"in-progress": [],
"testing": [],
"done": []
}

22
docs_mdbook/book.toml Normal file
View File

@ -0,0 +1,22 @@
[book]
authors = ["Daniel Seiller"]
language = "en"
multilingual = false
src = "src"
title = "Elite: Dangerous Long Range Router"
[output.html]
preferred-dark-theme = "ayu"
mathjax-support = true
[preprocessor.svgbob]
text_width = 8.0
text_height = 16.0
class = "bob"
font_family = "arial"
font_size = 14.0
stroke_width = 2.0
# there's using css-variables from theme:
stroke_color = "var(--fg)" # see default theme / variables.css
background_color = "transparent" # also useful `var(--bg)`
# all properties are optional.

View File

@ -0,0 +1,12 @@
# Summary
- [Intro](./intro/_index.md)
- [Galactic Travel in Elite: Dangerous](./intro/ed_travel.md)
- [FSD Fuel consumption and jump range](./intro/fsd_fuel.md)
- [Graph Search algorithms](./intro/graph_algos.md)
- [Elite Dangerous Long Range Router](./ed_lrr/_index.md)
- [Graph representation](./ed_lrr/graph.md)
- [Search modes](./ed_lrr/modes.md)
- [Graph precomputation](./ed_lrr/precomp.md)
- [Python API](./ed_lrr/py_api.md)

View File

@ -0,0 +1 @@
# Elite Dangerous Long Range Router

View File

@ -0,0 +1,4 @@
# Graph representation
ED_LRR uses an implicit graph built on top of an R*-Tree for its route computation.
Every node (star system) has edges towards all systems within jump range, edge weights (the distance between star systems) can be computed on the fly when necessary

View File

@ -0,0 +1,31 @@
# Search modes
## Heuristic
A*, Greedy and Beam search all use the following heuristic to select candidates to expand on the next layer of the graph
$$mult(n) =
\begin{cases}
4 &\text{if $n$ is a neutron star} \\\\
1.5 &\text{if $n$ is a white dwarf star} \\\\
1 &\text{otherwise}
\end{cases}
$$
$$d(a,b) = \sqrt{(a_x-b_x)^2+(a_y-b_y)^2+(a_z-b_z)^2}$$
$$
h(\text{node},\text{goal})=
\max(
d(\text{node},\text{goal})-
(
\text{range}*mult(\text{node})
),0)
$$
potential new heuristic:
$$
h(\text{node},\text{next},\text{goal})=
1 - {\cos^{-1}(|(\text{next}-\text{node})| \cdot |(\text{goal}-\text{node})|)\over\pi}
$$

View File

@ -0,0 +1,2 @@
# Graph precomputation

View File

@ -0,0 +1,58 @@
# Python API
First the module needs to be imported
```python
from _ed_lrr import PyRouter, PyShip
```
Then we need to instantiate a route plotter
```python
# callback is passed a dict describing the current search state and progress
def callback(state):
print(state)
r=PyRouter(callback)
```
Optionally ship loadouts can be loaded from the Elite: Dangerous journal files
```python
ships = PyShip.from_journal()
```
To plot a route we need to load a list of star systems with coordinates
```python
r.load("./stars.csv")
```
After a list has been loaded we can resolve star systems to their IDs
```python
systems = [
# resolve by coordinates, needs to build an R*-Tree so uses a few GB of RAM
(0,0,0),
# resolve by name, does fuzzy search, has to scan the whole list of system names
"Colonia",
# resolve by ID, fairly fast, but the IDs are ed_lrr specific
3553323
]
systems = r.resolve_systems(*query) # this will return a dict mapping the input key to a dict
assert sorted(systems.keys())==sorted(query)
sys_ids = {k: v["id"] for k, v in systems.items()}
```
Once the system IDs are known we can compute a route
```python
route = r.route(
[sys_ids["Sol"], sys_ids["Colonia"]] # route hops, can be any number of systems (at least 2)
48.0, # jump range
beam_width=1<<12, # beam width to limit exploration (4096)
greedyness=0, # greedyness for A* (0=BFS,0.5 = A*, 1=Greedy)
max_dist=500, # maximum deviation from straight line (not yet implemented)
num_workers=0 # number of workers to distribute the search accross (0 -> serial mode, >=1 -> spawn worker pool)
)
```

17
docs_mdbook/src/fsd.asy Normal file
View File

@ -0,0 +1,17 @@
import graph;
size(500,0);
real fsd(real m_fuel) {
// 4A drive
real boost = 1.0;
real f_max = 3.0;
real m_opt = 525.0;
real m_ship = 347.0;
real l = 12.0;
real p = 2.30;
return ((boost*m_opt*(1000.0*min(f_max,m_fuel)/l)^(1/p)))/(m_ship+m_fuel);
}
draw(graph(fsd,-100,100,n=500,Hermite),red);
xaxis("$m_{fuel}$");
yaxis("$range (Ly)$",0);

View File

@ -0,0 +1 @@
# Intro

View File

@ -0,0 +1,19 @@
# Galactic Travel in Elite: Dangerous
All ships in Elite: Dangerous (E:D) are equipped with a Frame Shift Drive (FSD) which allows them to jumpst vast distances (multiple light years) from one star system to another.
The maximum range you can traverse in a single jump is limited by the maximum fuel consuption per jump of the specific drive (depends on class and rating) and influenced by the following factors:
- Rating of the FSD
- Class of the FSD
- Mass of your ship (Base mass+Cargo mass+Fuel mass)
- Amount of fuel available in the tank
For details see [the chapter detailing FSD fuel consumption](./fsd_fuel.html)
If the ship is equipped with a Fuel Scoop it can:
- Scoop hydrogen from the corona of a star to refill its fuel tank (only applies to stars of class K, G, B, F, O, A or M)
- Supercharge its FSD to increase the maximum jump range by a factor of:
- 4 if supercharging in the jets of a neutron star
- 1.5 if supercharging in the jets of a white dwarf star

View File

@ -0,0 +1,36 @@
# Notes on FSD Fuel consumption and jump range
FSD Fuel consumption ([Elite: Dangerous Wiki](https://elite-dangerous.fandom.com/wiki/Frame_Shift_Drive#Hyperspace_Fuel_Equation)):
$$Fuel = 0.001 \cdot l \cdot \left(\frac{dist \cdot \left(m_{fuel} + m_{ship}\right)}{boost \cdot m_{opt}}\right)^{p}$$
Solving for \\(dist\\) gives the jump range (in Ly) for a given amount of fuel (in tons) as:
$$dist = \frac{boost \cdot m_{opt} \cdot \left(\frac{1000.0 \cdot \min\left(f_{max}, m_{fuel}\right)}{l}\right)^{\frac{1}{p}}}{m_{fuel} + m_{ship}}$$
Assuming \\(f_{max}\\) tons of available fuel gives us the maximum jump range for a single jump as:
$$dist_{max} = \frac{boost \cdot m_{opt} \cdot \left(\frac{1000.0 \cdot f_{max}}{l}\right)^{\frac{1}{p}}}{f_{max} + m_{ship}}$$
Since the guardian FSD booster increases the maximum jump range by \\(B_g\\) light years we can calculate a correction factor for the fuel consumption as:
$$ e_{fuel} = 0.001 \cdot l \cdot \left(\frac{boost^{2} \cdot m_{opt} \cdot \left(\frac{1000.0 \cdot \min\left(f_{max}, m_{fuel}\right)}{l}\right)^{\frac{1}{p}}}{B_{g} \cdot \left(m_{fuel} + m_{ship}\right) + boost^{2} \cdot m_{opt} \cdot \left(\frac{1000.0 \cdot \min\left(f_{max}, m_{fuel}\right)}{l}\right)^{\frac{1}{p}}}\right)^{p}$$
Incorporating \\(e_{fuel}\\) into the distance equation yields
$$dist = \frac{boost \cdot m_{opt} \cdot \left(\frac{1000.0 \cdot e_{fuel} \cdot \min\left(f_{max}, m_{fuel}\right)}{l}\right)^{\frac{1}{p}}}{m_{fuel} + m_{ship}}$$
Expanding \\(e_{fuel}\\) yields
$$dist = \frac{boost \cdot m_{opt} \cdot \left(1.0 \cdot \left(\frac{boost^{2} \cdot m_{opt} \cdot \left(\frac{1000.0 \cdot \min\left(f_{max}, m_{fuel}\right)}{l}\right)^{\frac{1}{p}}}{B_{g} \cdot \left(m_{fuel} + m_{ship}\right) + boost^{2} \cdot m_{opt} \cdot \left(\frac{1000.0 \cdot \min\left(f_{max}, m_{fuel}\right)}{l}\right)^{\frac{1}{p}}}\right)^{p} \cdot \min\left(f_{max}, m_{fuel}\right)\right)^{\frac{1}{p}}}{m_{fuel} + m_{ship}}$$
Finally, Expanding \\(dist_{max}\\) yields the full equation as
$$dist = \frac{boost \cdot m_{opt} \cdot \left(\frac{1000000.0 \cdot \left(\frac{boost^{2} \cdot m_{opt} \cdot \left(\frac{1000.0 \cdot \min\left(f_{max}, m_{fuel}\right)}{l}\right)^{\frac{1}{p}}}{B_{g} \cdot \left(m_{fuel} + m_{ship}\right) + boost^{2} \cdot m_{opt} \cdot \left(\frac{1000.0 \cdot \min\left(f_{max}, m_{fuel}\right)}{l}\right)^{\frac{1}{p}}}\right)^{- p} \cdot \min\left(f_{max}, m_{fuel}\right)}{l^{2}}\right)^{\frac{1}{p}}}{m_{fuel} + m_{ship}}$$
Where:
- \\(Fuel\\) is the fuel needed to jump (in tons)
- \\(l\\) is the linear constant of your FSD (depends on the rating)
- \\(p\\) is the power constant of your FSD (depends on the class)
- \\(m_{ship}\\) is the mass of your ship (including cargo)
- \\(m_{fuel}\\) is the amount of fuel in tons currently stored in your tanks
- \\(m_{opt}\\) is the optimized mass of your FSD (in tons)
- \\(f_{max}\\) is the maximum amount of fuel your FSD can use per jump
- \\(boost\\) is the "boost factor" of your FSD (1.0 when jumping normally, 1.5 when supercharged by a white dwarf, 4.0 for a neutron star, etc)
- \\(dist\\) is the distance you can jump with a given fuel amount
- \\(dist_{max}\\) is the maximum distance you can jump (when \\(m_{fuel}=f_{max}\\))
- \\(B_{g}\\) is the amount of Ly added by your Guardian FSD Booster
- \\(e_{fuel}\\) is the efficiency increase added by the Guardian FSD Booster

View File

@ -0,0 +1,25 @@
# Graph Search algorithms
## Breadth-first search (BFS)
BFS expand node in breadth first order while keeping track of the parent node of each expanded node
## Beam search
Beam search is similar to BFS but limits the number of expanded nodes based on a heuristic
## Greedy search
Greedy search is essentially Beam search with a beam width of 1
## Dijkstra
Dijkstra's algorithm finds the shortest path across a graph based on some edge weight
## A*
A* is similar to Dijkstra but uses a heuristic to speed up the search
## Beam-Stack search (BSS)
Beam-Stack search is a variation of beam search which keeps a separate priority queue for each layer of the graph to allow backtracking and expand previously unexpanded nodes

47
docs_mdbook/src/range.asy Normal file
View File

@ -0,0 +1,47 @@
import graph;
import stats;
size(500);
srand(10);
struct Star {
pair pos;
real mult;
}
real range = 48.0;
int n_stars=1000;
Star[] stars=new Star[n_stars];
for(int i=0; i < n_stars; ++i) {
Star s=new Star;
s.pos=(Gaussrand()*range*2,Gaussrand()*range*2);
s.mult=1.0;
if (unitrand()<0.2) {
s.mult=1.5;
} else {
if (unitrand()<0.1) {
s.mult=4.0;
}
}
stars[i]=s;
}
Star origin=new Star;
origin.pos=(0,0);
origin.mult=1.0;
draw(circle(origin.pos,range*origin.mult),white);
draw(circle(origin.pos,range),white+dashed);
draw(circle(origin.pos,range*2),white+dashed);
draw(circle(origin.pos,range*4),white+dashed);
fill(circle(origin.pos,2),red);
for (Star s: stars) {
if (length(s.pos-origin.pos)<(range*origin.mult)) {
draw(s.pos--origin.pos,white+dashed);
fill(circle(s.pos,s.mult),green);
} else {
fill(circle(s.pos,s.mult),white);
}
};

24
ed_lrr_gui.code-workspace Normal file
View File

@ -0,0 +1,24 @@
{
"folders": [
{
"path": "."
},
{
"path": "rust"
}
],
"settings": {
"discord.enabled": true,
"files.exclude": {
"**/.git": true,
"**/.svn": true,
"**/.hg": true,
"**/CVS": true,
"**/.DS_Store": true,
"**/Thumbs.db": true,
"**/.history": true,
".history": true
},
"explorerExclude.backup": null
}
}

View File

@ -304,15 +304,7 @@ def preprocess(systems, bodies, output):
"-g",
metavar="<float>",
default=cfg["route.greediness"],
help="Greedyness factor for A-Star",
show_default=True,
)
@click.option(
"--mode",
"-m",
default=cfg["route.mode"],
help="Search mode",
type=click.Choice(["bfs", "bfs_old", "a-star", "greedy"]),
help="Greedyness factor (0.0=BFS, 1.0=Greedy)",
show_default=True,
)
@click.option(

108
fsd_eq.py Normal file
View File

@ -0,0 +1,108 @@
from sympy import *
from sympy.utilities.codegen import RustCodeGen
def to_latex(eq, inline=False):
mode = "equation"
itex = True
if inline:
mode = "inline"
itex = False
return latex(eq, mul_symbol=" \\cdot ", mode=mode, itex=itex)
def solve_for(eq, sym):
return Eq(sym, solve(eq, sym)[0])
init_printing(use_latex=True, latex_mode="equation", mul_symbol="\\cdot")
var("m_ship m_fuel m_opt f_useable f_max l p boost dist dist_max Fuel B_g e_fuel")
mass = m_ship + m_fuel # total mass of ship+fuel
m_opt = m_opt * boost # supercharging increases optimized mass
available_fuel = Min(
f_max, m_fuel
) # limit maximum fuel consumption to FSD max fuel limit
eq_fuel = Eq(Fuel, l * 0.001 * (((dist * mass) / m_opt) ** p)) # FSD Fuel equation
eq_fuel_boost = eq_fuel.subs({"dist":dist+B_g,"Fuel":available_fuel*e_fuel}) # FSD Booster boosts maximum distance by B_g
eq_d_boost = solve_for(eq_fuel_boost, dist) # solve for distance
# eq_d_boost = eq_d_boost.subs({"Fuel":f_max,"m_fuel":f_max}) # Assume maximum jump range
print(to_latex(eq_d_boost))
print(to_latex(solve_for(eq_d_boost,e_fuel)))
exit()
max_range = eq_d.subs(
{m_fuel: f_max, dist: dist_max}
) # Compute maximum jump range by assuming f_max tons of fuel in tank
full_eq = eq_d.subs(
Min(f_max, m_fuel), Min(f_max, m_fuel) * fuel_mult
).subs(
max_range.lhs, max_range.rhs
) # substitute everything in
docs = [
(
eq_fuel,
"FSD Fuel consumption ([E:D Wiki](https://elite-dangerous.fandom.com/wiki/Frame_Shift_Drive#Hyperspace_Fuel_Equation)):",
),
(
eq_d,
"Solving for $dist$ gives the jump range (in Ly) for a given amount of fuel (in tons) as:",
),
(
max_range,
"Assuming $f_{max}$ tons of available fuel gives us the maximum jump range for a single jump as:",
),
(
fuel_mult,
"Since the guardian FSD booster increases the maximum jump range by $B_g$ Ly we can calculate a correction factor for the fuel consumption as:",
),
(
eq_d.subs(Min(f_max, m_fuel), Min(f_max, m_fuel) * e_fuel),
"Incorporating $e_{fuel}$ into the distance equation yields",
),
(
eq_d.subs(Min(f_max, m_fuel), Min(f_max, m_fuel) * fuel_mult),
"Expanding $e_{fuel}$ yields",
),
(full_eq, "Finally, Expanding $dist_{max}$ yields the full equation as"),
]
var_defs = [
("Fuel", "is the fuel needed to jump (in tons)"),
("l", "is the linear constant of your FSD (depends on the rating)"),
("p", "is the power constant of your FSD (depends on the class)"),
("m_ship", "is the mass of your ship (including cargo)"),
("m_fuel", "is the amount of fuel in tons currently stored in your tanks"),
("m_opt", "is the optimized mass of your FSD (in tons)"),
("f_max", "is the maximum amount of fuel your FSD can use per jump"),
(
"boost",
'is the "boost factor" of your FSD (1.0 when jumping normally, 1.5 when supercharged by a white dwarf, 4.0 for a neutron star, etc)',
),
("dist", "is the distance you can jump with a given fuel amount"),
("dist_max", "is the maximum distance you can jump (when $m_{fuel}=f_{max}$)"),
("B_g", "is the amount of Ly added by your Guardian FSD Booster"),
("e_fuel", "is the efficiency increase added by the Guardian FSD Booster"),
]
for eq, doc in docs:
eq=simplify(eq)
if doc:
print(doc, to_latex(eq))
else:
print(to_latex(eq))
print()
print("Where:")
for name, desc in var_defs:
print("- {} {}".format(to_latex(symbols(name), True), desc))

904
heuristic_vis.ipynb Normal file
View File

@ -0,0 +1,904 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Populating the interactive namespace from numpy and matplotlib\n"
]
}
],
"source": [
"%pylab notebook"
]
},
{
"cell_type": "code",
"execution_count": 131,
"metadata": {},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"<ipython-input-131-1f27bb9e71ac>:5: RuntimeWarning: invalid value encountered in double_scalars\n",
" ld = (((a+b+c)*(-a+b+c)*(a-b+c)*(a+b-c))**0.5)/(c*2.0)\n"
]
},
{
"data": {
"application/javascript": [
"/* Put everything inside the global mpl namespace */\n",
"window.mpl = {};\n",
"\n",
"\n",
"mpl.get_websocket_type = function() {\n",
" if (typeof(WebSocket) !== 'undefined') {\n",
" return WebSocket;\n",
" } else if (typeof(MozWebSocket) !== 'undefined') {\n",
" return MozWebSocket;\n",
" } else {\n",
" alert('Your browser does not have WebSocket support. ' +\n",
" 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n",
" 'Firefox 4 and 5 are also supported but you ' +\n",
" 'have to enable WebSockets in about:config.');\n",
" };\n",
"}\n",
"\n",
"mpl.figure = function(figure_id, websocket, ondownload, parent_element) {\n",
" this.id = figure_id;\n",
"\n",
" this.ws = websocket;\n",
"\n",
" this.supports_binary = (this.ws.binaryType != undefined);\n",
"\n",
" if (!this.supports_binary) {\n",
" var warnings = document.getElementById(\"mpl-warnings\");\n",
" if (warnings) {\n",
" warnings.style.display = 'block';\n",
" warnings.textContent = (\n",
" \"This browser does not support binary websocket messages. \" +\n",
" \"Performance may be slow.\");\n",
" }\n",
" }\n",
"\n",
" this.imageObj = new Image();\n",
"\n",
" this.context = undefined;\n",
" this.message = undefined;\n",
" this.canvas = undefined;\n",
" this.rubberband_canvas = undefined;\n",
" this.rubberband_context = undefined;\n",
" this.format_dropdown = undefined;\n",
"\n",
" this.image_mode = 'full';\n",
"\n",
" this.root = $('<div/>');\n",
" this._root_extra_style(this.root)\n",
" this.root.attr('style', 'display: inline-block');\n",
"\n",
" $(parent_element).append(this.root);\n",
"\n",
" this._init_header(this);\n",
" this._init_canvas(this);\n",
" this._init_toolbar(this);\n",
"\n",
" var fig = this;\n",
"\n",
" this.waiting = false;\n",
"\n",
" this.ws.onopen = function () {\n",
" fig.send_message(\"supports_binary\", {value: fig.supports_binary});\n",
" fig.send_message(\"send_image_mode\", {});\n",
" if (mpl.ratio != 1) {\n",
" fig.send_message(\"set_dpi_ratio\", {'dpi_ratio': mpl.ratio});\n",
" }\n",
" fig.send_message(\"refresh\", {});\n",
" }\n",
"\n",
" this.imageObj.onload = function() {\n",
" if (fig.image_mode == 'full') {\n",
" // Full images could contain transparency (where diff images\n",
" // almost always do), so we need to clear the canvas so that\n",
" // there is no ghosting.\n",
" fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n",
" }\n",
" fig.context.drawImage(fig.imageObj, 0, 0);\n",
" };\n",
"\n",
" this.imageObj.onunload = function() {\n",
" fig.ws.close();\n",
" }\n",
"\n",
" this.ws.onmessage = this._make_on_message_function(this);\n",
"\n",
" this.ondownload = ondownload;\n",
"}\n",
"\n",
"mpl.figure.prototype._init_header = function() {\n",
" var titlebar = $(\n",
" '<div class=\"ui-dialog-titlebar ui-widget-header ui-corner-all ' +\n",
" 'ui-helper-clearfix\"/>');\n",
" var titletext = $(\n",
" '<div class=\"ui-dialog-title\" style=\"width: 100%; ' +\n",
" 'text-align: center; padding: 3px;\"/>');\n",
" titlebar.append(titletext)\n",
" this.root.append(titlebar);\n",
" this.header = titletext[0];\n",
"}\n",
"\n",
"\n",
"\n",
"mpl.figure.prototype._canvas_extra_style = function(canvas_div) {\n",
"\n",
"}\n",
"\n",
"\n",
"mpl.figure.prototype._root_extra_style = function(canvas_div) {\n",
"\n",
"}\n",
"\n",
"mpl.figure.prototype._init_canvas = function() {\n",
" var fig = this;\n",
"\n",
" var canvas_div = $('<div/>');\n",
"\n",
" canvas_div.attr('style', 'position: relative; clear: both; outline: 0');\n",
"\n",
" function canvas_keyboard_event(event) {\n",
" return fig.key_event(event, event['data']);\n",
" }\n",
"\n",
" canvas_div.keydown('key_press', canvas_keyboard_event);\n",
" canvas_div.keyup('key_release', canvas_keyboard_event);\n",
" this.canvas_div = canvas_div\n",
" this._canvas_extra_style(canvas_div)\n",
" this.root.append(canvas_div);\n",
"\n",
" var canvas = $('<canvas/>');\n",
" canvas.addClass('mpl-canvas');\n",
" canvas.attr('style', \"left: 0; top: 0; z-index: 0; outline: 0\")\n",
"\n",
" this.canvas = canvas[0];\n",
" this.context = canvas[0].getContext(\"2d\");\n",
"\n",
" var backingStore = this.context.backingStorePixelRatio ||\n",
"\tthis.context.webkitBackingStorePixelRatio ||\n",
"\tthis.context.mozBackingStorePixelRatio ||\n",
"\tthis.context.msBackingStorePixelRatio ||\n",
"\tthis.context.oBackingStorePixelRatio ||\n",
"\tthis.context.backingStorePixelRatio || 1;\n",
"\n",
" mpl.ratio = (window.devicePixelRatio || 1) / backingStore;\n",
"\n",
" var rubberband = $('<canvas/>');\n",
" rubberband.attr('style', \"position: absolute; left: 0; top: 0; z-index: 1;\")\n",
"\n",
" var pass_mouse_events = true;\n",
"\n",
" canvas_div.resizable({\n",
" start: function(event, ui) {\n",
" pass_mouse_events = false;\n",
" },\n",
" resize: function(event, ui) {\n",
" fig.request_resize(ui.size.width, ui.size.height);\n",
" },\n",
" stop: function(event, ui) {\n",
" pass_mouse_events = true;\n",
" fig.request_resize(ui.size.width, ui.size.height);\n",
" },\n",
" });\n",
"\n",
" function mouse_event_fn(event) {\n",
" if (pass_mouse_events)\n",
" return fig.mouse_event(event, event['data']);\n",
" }\n",
"\n",
" rubberband.mousedown('button_press', mouse_event_fn);\n",
" rubberband.mouseup('button_release', mouse_event_fn);\n",
" // Throttle sequential mouse events to 1 every 20ms.\n",
" rubberband.mousemove('motion_notify', mouse_event_fn);\n",
"\n",
" rubberband.mouseenter('figure_enter', mouse_event_fn);\n",
" rubberband.mouseleave('figure_leave', mouse_event_fn);\n",
"\n",
" canvas_div.on(\"wheel\", function (event) {\n",
" event = event.originalEvent;\n",
" event['data'] = 'scroll'\n",
" if (event.deltaY < 0) {\n",
" event.step = 1;\n",
" } else {\n",
" event.step = -1;\n",
" }\n",
" mouse_event_fn(event);\n",
" });\n",
"\n",
" canvas_div.append(canvas);\n",
" canvas_div.append(rubberband);\n",
"\n",
" this.rubberband = rubberband;\n",
" this.rubberband_canvas = rubberband[0];\n",
" this.rubberband_context = rubberband[0].getContext(\"2d\");\n",
" this.rubberband_context.strokeStyle = \"#000000\";\n",
"\n",
" this._resize_canvas = function(width, height) {\n",
" // Keep the size of the canvas, canvas container, and rubber band\n",
" // canvas in synch.\n",
" canvas_div.css('width', width)\n",
" canvas_div.css('height', height)\n",
"\n",
" canvas.attr('width', width * mpl.ratio);\n",
" canvas.attr('height', height * mpl.ratio);\n",
" canvas.attr('style', 'width: ' + width + 'px; height: ' + height + 'px;');\n",
"\n",
" rubberband.attr('width', width);\n",
" rubberband.attr('height', height);\n",
" }\n",
"\n",
" // Set the figure to an initial 600x600px, this will subsequently be updated\n",
" // upon first draw.\n",
" this._resize_canvas(600, 600);\n",
"\n",
" // Disable right mouse context menu.\n",
" $(this.rubberband_canvas).bind(\"contextmenu\",function(e){\n",
" return false;\n",
" });\n",
"\n",
" function set_focus () {\n",
" canvas.focus();\n",
" canvas_div.focus();\n",
" }\n",
"\n",
" window.setTimeout(set_focus, 100);\n",
"}\n",
"\n",
"mpl.figure.prototype._init_toolbar = function() {\n",
" var fig = this;\n",
"\n",
" var nav_element = $('<div/>');\n",
" nav_element.attr('style', 'width: 100%');\n",
" this.root.append(nav_element);\n",
"\n",
" // Define a callback function for later on.\n",
" function toolbar_event(event) {\n",
" return fig.toolbar_button_onclick(event['data']);\n",
" }\n",
" function toolbar_mouse_event(event) {\n",
" return fig.toolbar_button_onmouseover(event['data']);\n",
" }\n",
"\n",
" for(var toolbar_ind in mpl.toolbar_items) {\n",
" var name = mpl.toolbar_items[toolbar_ind][0];\n",
" var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
" var image = mpl.toolbar_items[toolbar_ind][2];\n",
" var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
"\n",
" if (!name) {\n",
" // put a spacer in here.\n",
" continue;\n",
" }\n",
" var button = $('<button/>');\n",
" button.addClass('ui-button ui-widget ui-state-default ui-corner-all ' +\n",
" 'ui-button-icon-only');\n",
" button.attr('role', 'button');\n",
" button.attr('aria-disabled', 'false');\n",
" button.click(method_name, toolbar_event);\n",
" button.mouseover(tooltip, toolbar_mouse_event);\n",
"\n",
" var icon_img = $('<span/>');\n",
" icon_img.addClass('ui-button-icon-primary ui-icon');\n",
" icon_img.addClass(image);\n",
" icon_img.addClass('ui-corner-all');\n",
"\n",
" var tooltip_span = $('<span/>');\n",
" tooltip_span.addClass('ui-button-text');\n",
" tooltip_span.html(tooltip);\n",
"\n",
" button.append(icon_img);\n",
" button.append(tooltip_span);\n",
"\n",
" nav_element.append(button);\n",
" }\n",
"\n",
" var fmt_picker_span = $('<span/>');\n",
"\n",
" var fmt_picker = $('<select/>');\n",
" fmt_picker.addClass('mpl-toolbar-option ui-widget ui-widget-content');\n",
" fmt_picker_span.append(fmt_picker);\n",
" nav_element.append(fmt_picker_span);\n",
" this.format_dropdown = fmt_picker[0];\n",
"\n",
" for (var ind in mpl.extensions) {\n",
" var fmt = mpl.extensions[ind];\n",
" var option = $(\n",
" '<option/>', {selected: fmt === mpl.default_extension}).html(fmt);\n",
" fmt_picker.append(option);\n",
" }\n",
"\n",
" // Add hover states to the ui-buttons\n",
" $( \".ui-button\" ).hover(\n",
" function() { $(this).addClass(\"ui-state-hover\");},\n",
" function() { $(this).removeClass(\"ui-state-hover\");}\n",
" );\n",
"\n",
" var status_bar = $('<span class=\"mpl-message\"/>');\n",
" nav_element.append(status_bar);\n",
" this.message = status_bar[0];\n",
"}\n",
"\n",
"mpl.figure.prototype.request_resize = function(x_pixels, y_pixels) {\n",
" // Request matplotlib to resize the figure. Matplotlib will then trigger a resize in the client,\n",
" // which will in turn request a refresh of the image.\n",
" this.send_message('resize', {'width': x_pixels, 'height': y_pixels});\n",
"}\n",
"\n",
"mpl.figure.prototype.send_message = function(type, properties) {\n",
" properties['type'] = type;\n",
" properties['figure_id'] = this.id;\n",
" this.ws.send(JSON.stringify(properties));\n",
"}\n",
"\n",
"mpl.figure.prototype.send_draw_message = function() {\n",
" if (!this.waiting) {\n",
" this.waiting = true;\n",
" this.ws.send(JSON.stringify({type: \"draw\", figure_id: this.id}));\n",
" }\n",
"}\n",
"\n",
"\n",
"mpl.figure.prototype.handle_save = function(fig, msg) {\n",
" var format_dropdown = fig.format_dropdown;\n",
" var format = format_dropdown.options[format_dropdown.selectedIndex].value;\n",
" fig.ondownload(fig, format);\n",
"}\n",
"\n",
"\n",
"mpl.figure.prototype.handle_resize = function(fig, msg) {\n",
" var size = msg['size'];\n",
" if (size[0] != fig.canvas.width || size[1] != fig.canvas.height) {\n",
" fig._resize_canvas(size[0], size[1]);\n",
" fig.send_message(\"refresh\", {});\n",
" };\n",
"}\n",
"\n",
"mpl.figure.prototype.handle_rubberband = function(fig, msg) {\n",
" var x0 = msg['x0'] / mpl.ratio;\n",
" var y0 = (fig.canvas.height - msg['y0']) / mpl.ratio;\n",
" var x1 = msg['x1'] / mpl.ratio;\n",
" var y1 = (fig.canvas.height - msg['y1']) / mpl.ratio;\n",
" x0 = Math.floor(x0) + 0.5;\n",
" y0 = Math.floor(y0) + 0.5;\n",
" x1 = Math.floor(x1) + 0.5;\n",
" y1 = Math.floor(y1) + 0.5;\n",
" var min_x = Math.min(x0, x1);\n",
" var min_y = Math.min(y0, y1);\n",
" var width = Math.abs(x1 - x0);\n",
" var height = Math.abs(y1 - y0);\n",
"\n",
" fig.rubberband_context.clearRect(\n",
" 0, 0, fig.canvas.width / mpl.ratio, fig.canvas.height / mpl.ratio);\n",
"\n",
" fig.rubberband_context.strokeRect(min_x, min_y, width, height);\n",
"}\n",
"\n",
"mpl.figure.prototype.handle_figure_label = function(fig, msg) {\n",
" // Updates the figure title.\n",
" fig.header.textContent = msg['label'];\n",
"}\n",
"\n",
"mpl.figure.prototype.handle_cursor = function(fig, msg) {\n",
" var cursor = msg['cursor'];\n",
" switch(cursor)\n",
" {\n",
" case 0:\n",
" cursor = 'pointer';\n",
" break;\n",
" case 1:\n",
" cursor = 'default';\n",
" break;\n",
" case 2:\n",
" cursor = 'crosshair';\n",
" break;\n",
" case 3:\n",
" cursor = 'move';\n",
" break;\n",
" }\n",
" fig.rubberband_canvas.style.cursor = cursor;\n",
"}\n",
"\n",
"mpl.figure.prototype.handle_message = function(fig, msg) {\n",
" fig.message.textContent = msg['message'];\n",
"}\n",
"\n",
"mpl.figure.prototype.handle_draw = function(fig, msg) {\n",
" // Request the server to send over a new figure.\n",
" fig.send_draw_message();\n",
"}\n",
"\n",
"mpl.figure.prototype.handle_image_mode = function(fig, msg) {\n",
" fig.image_mode = msg['mode'];\n",
"}\n",
"\n",
"mpl.figure.prototype.updated_canvas_event = function() {\n",
" // Called whenever the canvas gets updated.\n",
" this.send_message(\"ack\", {});\n",
"}\n",
"\n",
"// A function to construct a web socket function for onmessage handling.\n",
"// Called in the figure constructor.\n",
"mpl.figure.prototype._make_on_message_function = function(fig) {\n",
" return function socket_on_message(evt) {\n",
" if (evt.data instanceof Blob) {\n",
" /* FIXME: We get \"Resource interpreted as Image but\n",
" * transferred with MIME type text/plain:\" errors on\n",
" * Chrome. But how to set the MIME type? It doesn't seem\n",
" * to be part of the websocket stream */\n",
" evt.data.type = \"image/png\";\n",
"\n",
" /* Free the memory for the previous frames */\n",
" if (fig.imageObj.src) {\n",
" (window.URL || window.webkitURL).revokeObjectURL(\n",
" fig.imageObj.src);\n",
" }\n",
"\n",
" fig.imageObj.src = (window.URL || window.webkitURL).createObjectURL(\n",
" evt.data);\n",
" fig.updated_canvas_event();\n",
" fig.waiting = false;\n",
" return;\n",
" }\n",
" else if (typeof evt.data === 'string' && evt.data.slice(0, 21) == \"data:image/png;base64\") {\n",
" fig.imageObj.src = evt.data;\n",
" fig.updated_canvas_event();\n",
" fig.waiting = false;\n",
" return;\n",
" }\n",
"\n",
" var msg = JSON.parse(evt.data);\n",
" var msg_type = msg['type'];\n",
"\n",
" // Call the \"handle_{type}\" callback, which takes\n",
" // the figure and JSON message as its only arguments.\n",
" try {\n",
" var callback = fig[\"handle_\" + msg_type];\n",
" } catch (e) {\n",
" console.log(\"No handler for the '\" + msg_type + \"' message type: \", msg);\n",
" return;\n",
" }\n",
"\n",
" if (callback) {\n",
" try {\n",
" // console.log(\"Handling '\" + msg_type + \"' message: \", msg);\n",
" callback(fig, msg);\n",
" } catch (e) {\n",
" console.log(\"Exception inside the 'handler_\" + msg_type + \"' callback:\", e, e.stack, msg);\n",
" }\n",
" }\n",
" };\n",
"}\n",
"\n",
"// from http://stackoverflow.com/questions/1114465/getting-mouse-location-in-canvas\n",
"mpl.findpos = function(e) {\n",
" //this section is from http://www.quirksmode.org/js/events_properties.html\n",
" var targ;\n",
" if (!e)\n",
" e = window.event;\n",
" if (e.target)\n",
" targ = e.target;\n",
" else if (e.srcElement)\n",
" targ = e.srcElement;\n",
" if (targ.nodeType == 3) // defeat Safari bug\n",
" targ = targ.parentNode;\n",
"\n",
" // jQuery normalizes the pageX and pageY\n",
" // pageX,Y are the mouse positions relative to the document\n",
" // offset() returns the position of the element relative to the document\n",
" var x = e.pageX - $(targ).offset().left;\n",
" var y = e.pageY - $(targ).offset().top;\n",
"\n",
" return {\"x\": x, \"y\": y};\n",
"};\n",
"\n",
"/*\n",
" * return a copy of an object with only non-object keys\n",
" * we need this to avoid circular references\n",
" * http://stackoverflow.com/a/24161582/3208463\n",
" */\n",
"function simpleKeys (original) {\n",
" return Object.keys(original).reduce(function (obj, key) {\n",
" if (typeof original[key] !== 'object')\n",
" obj[key] = original[key]\n",
" return obj;\n",
" }, {});\n",
"}\n",
"\n",
"mpl.figure.prototype.mouse_event = function(event, name) {\n",
" var canvas_pos = mpl.findpos(event)\n",
"\n",
" if (name === 'button_press')\n",
" {\n",
" this.canvas.focus();\n",
" this.canvas_div.focus();\n",
" }\n",
"\n",
" var x = canvas_pos.x * mpl.ratio;\n",
" var y = canvas_pos.y * mpl.ratio;\n",
"\n",
" this.send_message(name, {x: x, y: y, button: event.button,\n",
" step: event.step,\n",
" guiEvent: simpleKeys(event)});\n",
"\n",
" /* This prevents the web browser from automatically changing to\n",
" * the text insertion cursor when the button is pressed. We want\n",
" * to control all of the cursor setting manually through the\n",
" * 'cursor' event from matplotlib */\n",
" event.preventDefault();\n",
" return false;\n",
"}\n",
"\n",
"mpl.figure.prototype._key_event_extra = function(event, name) {\n",
" // Handle any extra behaviour associated with a key event\n",
"}\n",
"\n",
"mpl.figure.prototype.key_event = function(event, name) {\n",
"\n",
" // Prevent repeat events\n",
" if (name == 'key_press')\n",
" {\n",
" if (event.which === this._key)\n",
" return;\n",
" else\n",
" this._key = event.which;\n",
" }\n",
" if (name == 'key_release')\n",
" this._key = null;\n",
"\n",
" var value = '';\n",
" if (event.ctrlKey && event.which != 17)\n",
" value += \"ctrl+\";\n",
" if (event.altKey && event.which != 18)\n",
" value += \"alt+\";\n",
" if (event.shiftKey && event.which != 16)\n",
" value += \"shift+\";\n",
"\n",
" value += 'k';\n",
" value += event.which.toString();\n",
"\n",
" this._key_event_extra(event, name);\n",
"\n",
" this.send_message(name, {key: value,\n",
" guiEvent: simpleKeys(event)});\n",
" return false;\n",
"}\n",
"\n",
"mpl.figure.prototype.toolbar_button_onclick = function(name) {\n",
" if (name == 'download') {\n",
" this.handle_save(this, null);\n",
" } else {\n",
" this.send_message(\"toolbar_button\", {name: name});\n",
" }\n",
"};\n",
"\n",
"mpl.figure.prototype.toolbar_button_onmouseover = function(tooltip) {\n",
" this.message.textContent = tooltip;\n",
"};\n",
"mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home icon-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left icon-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right icon-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Pan axes with left mouse, zoom with right\", \"fa fa-arrows icon-move\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\", \"fa fa-square-o icon-check-empty\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o icon-save\", \"download\"]];\n",
"\n",
"mpl.extensions = [\"eps\", \"jpeg\", \"pdf\", \"png\", \"ps\", \"raw\", \"svg\", \"tif\"];\n",
"\n",
"mpl.default_extension = \"png\";var comm_websocket_adapter = function(comm) {\n",
" // Create a \"websocket\"-like object which calls the given IPython comm\n",
" // object with the appropriate methods. Currently this is a non binary\n",
" // socket, so there is still some room for performance tuning.\n",
" var ws = {};\n",
"\n",
" ws.close = function() {\n",
" comm.close()\n",
" };\n",
" ws.send = function(m) {\n",
" //console.log('sending', m);\n",
" comm.send(m);\n",
" };\n",
" // Register the callback with on_msg.\n",
" comm.on_msg(function(msg) {\n",
" //console.log('receiving', msg['content']['data'], msg);\n",
" // Pass the mpl event to the overridden (by mpl) onmessage function.\n",
" ws.onmessage(msg['content']['data'])\n",
" });\n",
" return ws;\n",
"}\n",
"\n",
"mpl.mpl_figure_comm = function(comm, msg) {\n",
" // This is the function which gets called when the mpl process\n",
" // starts-up an IPython Comm through the \"matplotlib\" channel.\n",
"\n",
" var id = msg.content.data.id;\n",
" // Get hold of the div created by the display call when the Comm\n",
" // socket was opened in Python.\n",
" var element = $(\"#\" + id);\n",
" var ws_proxy = comm_websocket_adapter(comm)\n",
"\n",
" function ondownload(figure, format) {\n",
" window.open(figure.imageObj.src);\n",
" }\n",
"\n",
" var fig = new mpl.figure(id, ws_proxy,\n",
" ondownload,\n",
" element.get(0));\n",
"\n",
" // Call onopen now - mpl needs it, as it is assuming we've passed it a real\n",
" // web socket which is closed, not our websocket->open comm proxy.\n",
" ws_proxy.onopen();\n",
"\n",
" fig.parent_element = element.get(0);\n",
" fig.cell_info = mpl.find_output_cell(\"<div id='\" + id + \"'></div>\");\n",
" if (!fig.cell_info) {\n",
" console.error(\"Failed to find cell for figure\", id, fig);\n",
" return;\n",
" }\n",
"\n",
" var output_index = fig.cell_info[2]\n",
" var cell = fig.cell_info[0];\n",
"\n",
"};\n",
"\n",
"mpl.figure.prototype.handle_close = function(fig, msg) {\n",
" var width = fig.canvas.width/mpl.ratio\n",
" fig.root.unbind('remove')\n",
"\n",
" // Update the output cell to use the data from the current canvas.\n",
" fig.push_to_output();\n",
" var dataURL = fig.canvas.toDataURL();\n",
" // Re-enable the keyboard manager in IPython - without this line, in FF,\n",
" // the notebook keyboard shortcuts fail.\n",
" IPython.keyboard_manager.enable()\n",
" $(fig.parent_element).html('<img src=\"' + dataURL + '\" width=\"' + width + '\">');\n",
" fig.close_ws(fig, msg);\n",
"}\n",
"\n",
"mpl.figure.prototype.close_ws = function(fig, msg){\n",
" fig.send_message('closing', msg);\n",
" // fig.ws.close()\n",
"}\n",
"\n",
"mpl.figure.prototype.push_to_output = function(remove_interactive) {\n",
" // Turn the data on the canvas into data in the output cell.\n",
" var width = this.canvas.width/mpl.ratio\n",
" var dataURL = this.canvas.toDataURL();\n",
" this.cell_info[1]['text/html'] = '<img src=\"' + dataURL + '\" width=\"' + width + '\">';\n",
"}\n",
"\n",
"mpl.figure.prototype.updated_canvas_event = function() {\n",
" // Tell IPython that the notebook contents must change.\n",
" IPython.notebook.set_dirty(true);\n",
" this.send_message(\"ack\", {});\n",
" var fig = this;\n",
" // Wait a second, then push the new image to the DOM so\n",
" // that it is saved nicely (might be nice to debounce this).\n",
" setTimeout(function () { fig.push_to_output() }, 1000);\n",
"}\n",
"\n",
"mpl.figure.prototype._init_toolbar = function() {\n",
" var fig = this;\n",
"\n",
" var nav_element = $('<div/>');\n",
" nav_element.attr('style', 'width: 100%');\n",
" this.root.append(nav_element);\n",
"\n",
" // Define a callback function for later on.\n",
" function toolbar_event(event) {\n",
" return fig.toolbar_button_onclick(event['data']);\n",
" }\n",
" function toolbar_mouse_event(event) {\n",
" return fig.toolbar_button_onmouseover(event['data']);\n",
" }\n",
"\n",
" for(var toolbar_ind in mpl.toolbar_items){\n",
" var name = mpl.toolbar_items[toolbar_ind][0];\n",
" var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
" var image = mpl.toolbar_items[toolbar_ind][2];\n",
" var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
"\n",
" if (!name) { continue; };\n",
"\n",
" var button = $('<button class=\"btn btn-default\" href=\"#\" title=\"' + name + '\"><i class=\"fa ' + image + ' fa-lg\"></i></button>');\n",
" button.click(method_name, toolbar_event);\n",
" button.mouseover(tooltip, toolbar_mouse_event);\n",
" nav_element.append(button);\n",
" }\n",
"\n",
" // Add the status bar.\n",
" var status_bar = $('<span class=\"mpl-message\" style=\"text-align:right; float: right;\"/>');\n",
" nav_element.append(status_bar);\n",
" this.message = status_bar[0];\n",
"\n",
" // Add the close button to the window.\n",
" var buttongrp = $('<div class=\"btn-group inline pull-right\"></div>');\n",
" var button = $('<button class=\"btn btn-mini btn-primary\" href=\"#\" title=\"Stop Interaction\"><i class=\"fa fa-power-off icon-remove icon-large\"></i></button>');\n",
" button.click(function (evt) { fig.handle_close(fig, {}); } );\n",
" button.mouseover('Stop Interaction', toolbar_mouse_event);\n",
" buttongrp.append(button);\n",
" var titlebar = this.root.find($('.ui-dialog-titlebar'));\n",
" titlebar.prepend(buttongrp);\n",
"}\n",
"\n",
"mpl.figure.prototype._root_extra_style = function(el){\n",
" var fig = this\n",
" el.on(\"remove\", function(){\n",
"\tfig.close_ws(fig, {});\n",
" });\n",
"}\n",
"\n",
"mpl.figure.prototype._canvas_extra_style = function(el){\n",
" // this is important to make the div 'focusable\n",
" el.attr('tabindex', 0)\n",
" // reach out to IPython and tell the keyboard manager to turn it's self\n",
" // off when our div gets focus\n",
"\n",
" // location in version 3\n",
" if (IPython.notebook.keyboard_manager) {\n",
" IPython.notebook.keyboard_manager.register_events(el);\n",
" }\n",
" else {\n",
" // location in version 2\n",
" IPython.keyboard_manager.register_events(el);\n",
" }\n",
"\n",
"}\n",
"\n",
"mpl.figure.prototype._key_event_extra = function(event, name) {\n",
" var manager = IPython.notebook.keyboard_manager;\n",
" if (!manager)\n",
" manager = IPython.keyboard_manager;\n",
"\n",
" // Check for shift+enter\n",
" if (event.shiftKey && event.which == 13) {\n",
" this.canvas_div.blur();\n",
" // select the cell after this one\n",
" var index = IPython.notebook.find_cell_index(this.cell_info[0]);\n",
" IPython.notebook.select(index + 1);\n",
" }\n",
"}\n",
"\n",
"mpl.figure.prototype.handle_save = function(fig, msg) {\n",
" fig.ondownload(fig, null);\n",
"}\n",
"\n",
"\n",
"mpl.find_output_cell = function(html_output) {\n",
" // Return the cell and output element which can be found *uniquely* in the notebook.\n",
" // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n",
" // IPython event is triggered only after the cells have been serialised, which for\n",
" // our purposes (turning an active figure into a static one), is too late.\n",
" var cells = IPython.notebook.get_cells();\n",
" var ncells = cells.length;\n",
" for (var i=0; i<ncells; i++) {\n",
" var cell = cells[i];\n",
" if (cell.cell_type === 'code'){\n",
" for (var j=0; j<cell.output_area.outputs.length; j++) {\n",
" var data = cell.output_area.outputs[j];\n",
" if (data.data) {\n",
" // IPython >= 3 moved mimebundle to data attribute of output\n",
" data = data.data;\n",
" }\n",
" if (data['text/html'] == html_output) {\n",
" return [cell, data, j];\n",
" }\n",
" }\n",
" }\n",
" }\n",
"}\n",
"\n",
"// Register the function which deals with the matplotlib target/channel.\n",
"// The kernel may be null if the page has been refreshed.\n",
"if (IPython.notebook.kernel != null) {\n",
" IPython.notebook.kernel.comm_manager.register_target('matplotlib', mpl.mpl_figure_comm);\n",
"}\n"
],
"text/plain": [
"<IPython.core.display.Javascript object>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"text/html": [
"<div id='215b26ff-80fe-45a3-bb52-9351ea1b2ebb'></div>"
],
"text/plain": [
"<IPython.core.display.HTML object>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"text/plain": [
"(0, 100)"
]
},
"execution_count": 131,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"def h(p,src,dest):\n",
" c = (((src-dest)**2).sum())**0.5\n",
" a = (((dest-p)**2).sum())**0.5\n",
" b = (((src-p)**2).sum())**0.5\n",
" ld = (((a+b+c)*(-a+b+c)*(a-b+c)*(a+b-c))**0.5)/(c*2.0)\n",
" return a-b\n",
"S=np.array([10,10])\n",
"D=np.array([20,25])\n",
"\n",
"Rx=range(100)\n",
"Ry=range(100)\n",
"grid=np.zeros((len(Rx),len(Ry)))\n",
"for px,x in enumerate(Rx):\n",
" for py,y in enumerate(Ry):\n",
" grid[px,py]=h(np.array([x,y]),S,D)\n",
"imshow(grid,cmap='coolwarm_r',origin='lower')\n",
"colorbar()\n",
"\n",
"scatter(*S,color='green')\n",
"scatter(*D,color='red')\n",
"plt.xlim(0,100)\n",
"plt.ylim(0,100)"
]
},
{
"cell_type": "code",
"execution_count": 38,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Object `np.magnitude` not found.\n"
]
}
],
"source": [
"np."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3.8.1 64-bit ('anaconda': conda)",
"language": "python",
"name": "python38164bitanacondaconda2a51168e890d45bd836f654eb2ae46f7"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.8.1"
}
},
"nbformat": 4,
"nbformat_minor": 4
}

View File

@ -1,6 +1,9 @@
# -*- coding: utf-8 -*-
import svgwrite
import svgpathtools
import random
import tempfile
import os
from math import sin, cos, pi
import tsp as m_tsp
@ -30,8 +33,6 @@ def make_points(n, size, min_dist=0):
px, py = random.random(), random.random()
px *= size / 2
py *= size / 2
px += 70
py += 70
valid = True
for p in points:
if dist2(p, (px, py)) < min_dist:
@ -60,9 +61,9 @@ def generate(seed, name=None, small=False):
size = 1000
if name is None:
name = seed
dwg = svgwrite.Drawing(filename="out/{}.svg".format(name))
dwg.defs.add(dwg.style(".background { fill: #222; }"))
dwg.add(dwg.rect(size=("100%", "100%"), class_="background"))
out_path = "out/{}.svg".format(name)
dwg = svgwrite.Drawing(filename=out_path)
dwg.defs.add(dwg.style(".background { fill: #222 }"))
print("Generating points...")
color = "#eee"
pos = make_points(num_points, size, min_dist=min_dist)
@ -74,12 +75,7 @@ def generate(seed, name=None, small=False):
x2 /= sd
y1 /= sd
y2 /= sd
dwg.add(dwg.line(
(x1, y1),
(x2, y2),
stroke_width=w,
stroke=color
))
dwg.add(dwg.line((x1, y1), (x2, y2), stroke_width=w, stroke=color))
for (px, py) in pos:
base_r = 3
@ -111,17 +107,13 @@ def generate(seed, name=None, small=False):
random.random()
random.random()
random.random()
random.random()
continue
r += ring_step(random.random())
ring_col = color
if random.random() > 0.75:
ring_col = "#ea0"
circ = dwg.add(dwg.circle(
(px, py),
r=r,
stroke_width=w,
stroke=ring_col
))
circ = dwg.add(dwg.circle((px, py), r=r, stroke_width=w, stroke=ring_col))
circ.fill(color, opacity=0)
d = random.random() * pi * 2
dx = cos(d)
@ -136,10 +128,27 @@ def generate(seed, name=None, small=False):
)
)
moon.fill(ring_col)
dwg.save()
dwg.fit()
path = tempfile.TemporaryDirectory()
filename = os.path.join(path.name, "out.svg")
dwg.saveas(filename)
paths, attrs = svgpathtools.svg2paths(filename)
bbox = [float("inf"), float("-inf"), float("inf"), float("-inf")]
for path in paths:
path_bbox = path.bbox()
bbox[0] = min(bbox[0], path_bbox[0]) # xmin
bbox[1] = max(bbox[1], path_bbox[1]) # xmax
bbox[2] = min(bbox[2], path_bbox[2]) # ymin
bbox[3] = max(bbox[3], path_bbox[3]) # ymax
px = bbox[0]
sx = (bbox[1] - bbox[0])
py = bbox[2]
sy = (bbox[3] - bbox[2])
dwg.add(dwg.rect(x=px, y=px, size=(sx, sy), class_="background"))
dwg.elements.insert(1, dwg.elements.pop(-1))
dwg.saveas(out_path)
seed = -4
seed = -5
generate(seed, "icon_1", small=False)
generate(seed, "icon_1_small", small=True)

BIN
icon/out/icon_1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

View File

@ -1,2 +1,2 @@
<?xml version="1.0" encoding="utf-8" ?>
<svg baseProfile="full" height="100%" version="1.1" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:ev="http://www.w3.org/2001/xml-events" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><style type="text/css"><![CDATA[.background { fill: #222; }]]></style></defs><rect class="background" height="100%" width="100%" x="0" y="0" /><line stroke="#eee" stroke-width="2" x1="188.02404486871725" x2="103.25754783979495" y1="121.5830171153579" y2="270.7955072425374" /><line stroke="#eee" stroke-width="2" x1="103.25754783979495" x2="528.9775215438594" y1="270.7955072425374" y2="470.2261757479043" /><line stroke="#eee" stroke-width="2" x1="528.9775215438594" x2="452.58130125271924" y1="470.2261757479043" y2="180.96408784515882" /><line stroke="#eee" stroke-width="2" x1="452.58130125271924" x2="338.3400040874068" y1="180.96408784515882" y2="208.34132172072512" /><circle cx="188.02404486871725" cy="121.5830171153579" fill="#eee" r="3.3185498772945903" stroke="#eee" stroke-width="2" /><circle cx="188.02404486871725" cy="121.5830171153579" fill="#eee" fill-opacity="0" r="22.429593602379832" stroke="#eee" stroke-width="2" /><circle cx="173.80179554276944" cy="104.23901834695114" fill="#eee" r="2.52050830126476" stroke="#eee" stroke-width="2" /><circle cx="103.25754783979495" cy="270.7955072425374" fill="#eee" r="3.4944547782059745" stroke="#eee" stroke-width="2" /><circle cx="103.25754783979495" cy="270.7955072425374" fill="#eee" fill-opacity="0" r="19.2697560241313" stroke="#eee" stroke-width="2" /><circle cx="115.03444741085107" cy="255.5433554690071" fill="#eee" r="3.7601015027223985" stroke="#eee" stroke-width="2" /><circle cx="103.25754783979495" cy="270.7955072425374" fill="#eee" fill-opacity="0" r="30.13693855048052" stroke="#eee" stroke-width="2" /><circle cx="89.02212054970202" cy="244.23260689141011" fill="#eee" r="3.0119075514208307" stroke="#eee" stroke-width="2" /><circle cx="528.9775215438594" cy="470.2261757479043" fill="#eee" r="4.420763662755435" stroke="#eee" stroke-width="2" /><circle cx="528.9775215438594" cy="470.2261757479043" fill="#eee" fill-opacity="0" r="22.44577790309402" stroke="#ea0" stroke-width="2" /><circle cx="549.9596596985477" cy="462.25354606049257" fill="#ea0" r="3.680925358835544" stroke="#ea0" stroke-width="2" /><circle cx="452.58130125271924" cy="180.96408784515882" fill="#eee" r="3.8758250081323116" stroke="#eee" stroke-width="2" /><circle cx="452.58130125271924" cy="180.96408784515882" fill="#eee" fill-opacity="0" r="21.8231723987879" stroke="#ea0" stroke-width="2" /><circle cx="430.78831758434035" cy="179.8166052191519" fill="#ea0" r="2.827892086263464" stroke="#ea0" stroke-width="2" /><circle cx="452.58130125271924" cy="180.96408784515882" fill="#eee" fill-opacity="0" r="37.812297120687795" stroke="#eee" stroke-width="2" /><circle cx="472.57653937753463" cy="213.0570818761791" fill="#eee" r="2.6102231928654778" stroke="#eee" stroke-width="2" /><circle cx="452.58130125271924" cy="180.96408784515882" fill="#eee" fill-opacity="0" r="55.938220307034" stroke="#eee" stroke-width="2" /><circle cx="506.1669380410402" cy="197.01600427617765" fill="#eee" r="3.252701491079807" stroke="#eee" stroke-width="2" /><circle cx="338.3400040874068" cy="208.34132172072512" fill="#eee" r="4.603865384638267" stroke="#eee" stroke-width="2" /><circle cx="338.3400040874068" cy="208.34132172072512" fill="#eee" fill-opacity="0" r="20.00878719559634" stroke="#eee" stroke-width="2" /><circle cx="329.11968037233845" cy="190.58358550160054" fill="#eee" r="2.132876938772122" stroke="#eee" stroke-width="2" /><circle cx="338.3400040874068" cy="208.34132172072512" fill="#eee" fill-opacity="0" r="39.144105385654704" stroke="#eee" stroke-width="2" /><circle cx="301.84139133159863" cy="222.48742554279568" fill="#eee" r="2.3674072974299003" stroke="#eee" stroke-width="2" /></svg>
<svg baseProfile="full" height="100%" preserveAspectRatio="xMidYMid meet" version="1.1" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:ev="http://www.w3.org/2001/xml-events" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><style type="text/css"><![CDATA[.background { fill: #222 }]]></style></defs><rect class="background" height="517.2983884727208" width="530.2849722660202" x="0" y="0" /><line stroke="#eee" stroke-width="2" x1="311.450847444851" x2="14.502614141807369" y1="370.89349463036467" y2="232.81132718905266" /><line stroke="#eee" stroke-width="2" x1="14.502614141807369" x2="450.45024587531134" y1="232.81132718905266" y2="56.60298232657218" /><line stroke="#eee" stroke-width="2" x1="450.45024587531134" x2="471.67835849915684" y1="56.60298232657218" y2="324.4872765684621" /><line stroke="#eee" stroke-width="2" x1="471.67835849915684" x2="397.5967827828483" y1="324.4872765684621" y2="471.2251418885252" /><circle cx="311.450847444851" cy="370.89349463036467" fill="#eee" r="3.739718497859491" stroke="#eee" stroke-width="2" /><circle cx="311.450847444851" cy="370.89349463036467" fill="#eee" fill-opacity="0" r="14.019744021739154" stroke="#eee" stroke-width="2" /><circle cx="309.7972050350797" cy="356.9716165524724" fill="#eee" r="2.816302109949028" stroke="#eee" stroke-width="2" /><circle cx="311.450847444851" cy="370.89349463036467" fill="#eee" fill-opacity="0" r="25.84050065951694" stroke="#ea0" stroke-width="2" /><circle cx="291.5987578747967" cy="387.4351394711004" fill="#ea0" r="3.5238508551887717" stroke="#ea0" stroke-width="2" /><circle cx="311.450847444851" cy="370.89349463036467" fill="#eee" fill-opacity="0" r="36.559818016667215" stroke="#eee" stroke-width="2" /><circle cx="277.06522171012307" cy="383.3131981792842" fill="#eee" r="2.2644817084338493" stroke="#eee" stroke-width="2" /><circle cx="14.502614141807369" cy="232.81132718905266" fill="#0ae" r="3.015939411732324" stroke="#0ae" stroke-width="2" /><circle cx="14.502614141807369" cy="232.81132718905266" fill="#eee" fill-opacity="0" r="22.601271002174997" stroke="#eee" stroke-width="2" /><circle cx="25.825648341003884" cy="252.3716530410824" fill="#eee" r="2.6273027354699074" stroke="#eee" stroke-width="2" /><circle cx="450.45024587531134" cy="56.60298232657218" fill="#eee" r="5.628356799732829" stroke="#eee" stroke-width="2" /><circle cx="450.45024587531134" cy="56.60298232657218" fill="#eee" fill-opacity="0" r="15.047795145337929" stroke="#ea0" stroke-width="2" /><circle cx="444.96726006047606" cy="42.5896670410885" fill="#ea0" r="3.933128624634391" stroke="#ea0" stroke-width="2" /><circle cx="450.45024587531134" cy="56.60298232657218" fill="#eee" fill-opacity="0" r="33.98521192110272" stroke="#eee" stroke-width="2" /><circle cx="428.59208164282967" cy="82.62634271119818" fill="#eee" r="2.331912114259491" stroke="#eee" stroke-width="2" /><circle cx="450.45024587531134" cy="56.60298232657218" fill="#eee" fill-opacity="0" r="45.44223101650954" stroke="#eee" stroke-width="2" /><circle cx="436.0392802753398" cy="99.69962291762357" fill="#eee" r="3.2062199948153087" stroke="#eee" stroke-width="2" /><circle cx="471.67835849915684" cy="324.4872765684621" fill="#eee" r="5.033802748643073" stroke="#eee" stroke-width="2" /><circle cx="471.67835849915684" cy="324.4872765684621" fill="#eee" fill-opacity="0" r="13.660224505510772" stroke="#eee" stroke-width="2" /><circle cx="466.96528173978857" cy="337.3086899461385" fill="#eee" r="3.3928396152823135" stroke="#eee" stroke-width="2" /><circle cx="471.67835849915684" cy="324.4872765684621" fill="#eee" fill-opacity="0" r="25.507956906495714" stroke="#eee" stroke-width="2" /><circle cx="483.09849262326327" cy="347.2959679410741" fill="#eee" r="2.5123636718854243" stroke="#eee" stroke-width="2" /><circle cx="397.5967827828483" cy="471.2251418885252" fill="#0ae" r="5.847661430011579" stroke="#0ae" stroke-width="2" /><circle cx="397.5967827828483" cy="471.2251418885252" fill="#eee" fill-opacity="0" r="21.448808893881296" stroke="#eee" stroke-width="2" /><circle cx="402.63513027636975" cy="450.3764858766675" fill="#eee" r="2.7323689516837213" stroke="#eee" stroke-width="2" /><circle cx="397.5967827828483" cy="471.2251418885252" fill="#eee" fill-opacity="0" r="37.23399718445004" stroke="#eee" stroke-width="2" /><circle cx="433.2375240152304" cy="482.0004892489591" fill="#eee" r="2.3618389759020957" stroke="#eee" stroke-width="2" /></svg>

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

BIN
icon/out/icon_1_pad.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

View File

@ -1,2 +1,2 @@
<?xml version="1.0" encoding="utf-8" ?>
<svg baseProfile="full" height="100%" version="1.1" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:ev="http://www.w3.org/2001/xml-events" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><style type="text/css"><![CDATA[.background { fill: #222; }]]></style></defs><rect class="background" height="100%" width="100%" x="0" y="0" /><line stroke="#eee" stroke-width="2" x1="94.01202243435863" x2="51.628773919897476" y1="60.79150855767895" y2="135.3977536212687" /><line stroke="#eee" stroke-width="2" x1="51.628773919897476" x2="264.4887607719297" y1="135.3977536212687" y2="235.11308787395214" /><line stroke="#eee" stroke-width="2" x1="264.4887607719297" x2="226.29065062635962" y1="235.11308787395214" y2="90.48204392257941" /><line stroke="#eee" stroke-width="2" x1="226.29065062635962" x2="169.1700020437034" y1="90.48204392257941" y2="104.17066086036256" /><circle cx="94.01202243435863" cy="60.79150855767895" fill="#eee" r="5.53091646215765" stroke="#eee" stroke-width="2" /><circle cx="51.628773919897476" cy="135.3977536212687" fill="#eee" r="6.358738772326545" stroke="#eee" stroke-width="2" /><circle cx="264.4887607719297" cy="235.11308787395214" fill="#0ae" r="9.400253756805997" stroke="#0ae" stroke-width="2" /><circle cx="226.29065062635962" cy="90.48204392257941" fill="#eee" r="6.236611100792434" stroke="#eee" stroke-width="2" /><circle cx="169.1700020437034" cy="104.17066086036256" fill="#eee" r="9.41158619939395" stroke="#eee" stroke-width="2" /></svg>
<svg baseProfile="full" height="100%" preserveAspectRatio="xMidYMid meet" version="1.1" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:ev="http://www.w3.org/2001/xml-events" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><style type="text/css"><![CDATA[.background { fill: #222 }]]></style></defs><rect class="background" height="246.4377768305505" width="267.0041091126337" x="0" y="0" /><line stroke="#eee" stroke-width="2" x1="155.7254237224255" x2="7.251307070903684" y1="185.44674731518234" y2="116.40566359452633" /><line stroke="#eee" stroke-width="2" x1="7.251307070903684" x2="225.22512293765567" y1="116.40566359452633" y2="28.30149116328609" /><line stroke="#eee" stroke-width="2" x1="225.22512293765567" x2="235.83917924957842" y1="28.30149116328609" y2="162.24363828423105" /><line stroke="#eee" stroke-width="2" x1="235.83917924957842" x2="198.79839139142416" y1="162.24363828423105" y2="235.6125709442626" /><circle cx="155.7254237224255" cy="185.44674731518234" fill="#eee" r="6.232864163099151" stroke="#eee" stroke-width="2" /><circle cx="7.251307070903684" cy="116.40566359452633" fill="#0ae" r="5.026565686220541" stroke="#0ae" stroke-width="2" /><circle cx="225.22512293765567" cy="28.30149116328609" fill="#eee" r="9.380594666221384" stroke="#eee" stroke-width="2" /><circle cx="235.83917924957842" cy="162.24363828423105" fill="#eee" r="8.389671247738455" stroke="#eee" stroke-width="2" /><circle cx="198.79839139142416" cy="235.6125709442626" fill="#0ae" r="9.74610238335263" stroke="#0ae" stroke-width="2" /></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

132
imgui_test/test.py Normal file
View File

@ -0,0 +1,132 @@
import dearpygui.core as dpg
import dearpygui.simple as sdpg
import uuid
import logging
from concurrent.futures import ProcessPoolExecutor
def setup_logging(loglevel="INFO"):
import coloredlogs
from datetime import timedelta
coloredlogs.DEFAULT_FIELD_STYLES["delta"] = {"color": "green"}
coloredlogs.DEFAULT_FIELD_STYLES["levelname"] = {"color": "yellow"}
class DeltaTimeFormatter(coloredlogs.ColoredFormatter):
def format(self, record):
seconds = record.relativeCreated / 1000
duration = timedelta(seconds=seconds)
record.delta = str(duration)
return super().format(record)
coloredlogs.ColoredFormatter = DeltaTimeFormatter
logfmt = " | ".join(
["[%(delta)s] %(levelname)s", "%(name)s:%(pathname)s:%(lineno)s", "%(message)s"]
)
numeric_level = getattr(logging, loglevel.upper(), None)
if not isinstance(numeric_level, int):
raise ValueError("Invalid log level: %s" % loglevel)
coloredlogs.install(level=numeric_level, fmt=logfmt)
setup_logging()
def resolve_systems(stars, *args):
import _ed_lrr
r = _ed_lrr.PyRouter(None)
r.load(stars)
return r.resolve_systems(*args)
class EdLrrGui:
def __init__(self):
self.pool = ProcessPoolExecutor(1)
self.systems = []
self.__resolve_job = None
self.logger = logging.getLogger("GUI")
def __set_state(self, state):
print(state)
dpg.set_value("ed_lrr_state", state)
def __resolve_done(self, fut):
data = fut.result()
self.logger.info(f"Gor resolver data back: {data}")
self.__resolve_job = None
for n, system in enumerate(self.systems):
if system["name"] in data:
self.systems[n] = data[system["name"]]
self.systems[n]["resolved"] = True
def __resolve_systems(self, sender, data):
names = []
for system in self.systems:
if not system.get("resolver", False):
names.append(system["name"])
if self.__resolve_job is None:
job = self.pool.submit(resolve_systems, "../stars.csv", *names)
self.logger.info(f"Resolving {len(names)} systems...")
self.__resolve_job = job
self.__resolve_job.add_done_callback(self.__resolve_done)
def __render(self, sender, data):
dpg.clear_table("Systems")
for system in self.systems:
row = [
system.get("id", ""),
system["name"],
", ".join(map(str, system.get("pos", []))),
]
dpg.add_row("Systems", row)
def __add_system(self, sender, data):
system_name = dpg.get_value("sys-name")
self.systems.append({"name": system_name})
dpg.set_value("sys-name", "")
def __select_system(self, sender, data):
system_row = dpg.get_table_selections("Systems")
idx = system_row[0][0]
dpg.add_data("selected-system-index", idx)
def __remove_system(self, sender, data):
if self.systems:
system_index = dpg.get_data("selected-system-index")
self.systems.pop(system_index)
def __clear_systems(self, sender, data):
self.systems = []
def show(self):
with sdpg.window("Main Window"):
dpg.set_main_window_size(550, 550)
dpg.set_main_window_resizable(False)
dpg.set_main_window_title("Elite: Dangerous Long Range Router")
dpg.add_text("ED_LRR")
dpg.add_separator()
dpg.add_input_text("System name", source="sys-name")
dpg.add_button("Add", callback=self.__add_system)
dpg.add_separator()
dpg.add_table(
"Systems",
["ID", "Name", "Position"],
height=200,
callback=self.__select_system,
)
dpg.add_separator()
dpg.add_button("Remove", callback=self.__remove_system)
dpg.add_button("Clear", callback=self.__clear_systems)
dpg.add_button("Resolve", callback=self.__resolve_systems)
# Render Callback and Start gui
dpg.set_render_callback(self.__render)
dpg.start_dearpygui(primary_window="Main Window")
if __name__ == "__main__":
edlrr_gui = EdLrrGui()
edlrr_gui.show()

View File

@ -0,0 +1 @@
{"route": [], "dt": 292.124997}

839494
logs/route_log_beam_0.txt Normal file

File diff suppressed because it is too large Load Diff

View File

@ -28,14 +28,14 @@ versions += ["3"]
nox.options.keywords = "test"
@nox.session(venv_backend="conda")
@nox.session(python=versions,venv_backend="conda")
def devenv(session):
"""Set up development environment"""
global path
location = os.path.abspath(session._runner.venv.location_name)
session.env["PATH"] = os.pathsep.join([location, path, location])
session.conda_install("pycrypto", "ujson")
session.install("--no-cache-dir", "-e",".[all]")
session.install("--no-cache-dir", "-e",".")
logger.warning(f'Devenv set up, now run "conda activate {location}"')

64
process_route_log.rs Normal file
View File

@ -0,0 +1,64 @@
// cargo-deps: crossbeam-channel="0.5.1"
extern crate crossbeam_channel;
use std::io::{BufReader,BufRead, BufWriter,Write};
use std::fs::File;
use std::collections::HashMap;
use std::collections::HashSet;
use crossbeam_channel::unbounded;
use crossbeam_channel::Receiver;
use std::thread;
fn process(rx: Receiver<String>) -> (HashMap<usize,usize>,HashMap<usize,usize>) {
let mut hm_min: HashMap<usize,usize> = HashMap::new();
let mut hm_max: HashMap<usize,usize> = HashMap::new();
while let Ok(line) = rx.recv() {
let line: Vec<usize> = line.split(",").map(|s| s.parse::<usize>().unwrap()).collect();
let id=line[0];
let mut depth=line[1];
hm_min.entry(id).and_modify(|e| {
*e=*e.min(&mut depth);
}).or_insert(depth);
hm_max.entry(id).and_modify(|e| {
*e=*e.max(&mut depth);
}).or_insert(depth);
}
(hm_min,hm_max)
}
fn main() {
let (tx,rx) = unbounded();
let mut threads: Vec<_> = (0..8).map(|_| {
let rx=rx.clone();
thread::spawn(|| {
process(rx)
})
}).collect();
let fh = BufReader::new(File::open(std::env::args().nth(1).unwrap()).unwrap());
fh.lines().flatten().for_each(|line| {
tx.send(line).unwrap();
});
drop(tx);
let mut hm_min: HashMap<usize,usize> = HashMap::new();
let mut hm_max: HashMap<usize,usize> = HashMap::new();
for thread in threads.drain(..) {
let (min,max)=thread.join().unwrap();
println!("Thread: {:?}",(min.len(),max.len()));
for (id,depth) in min {
hm_min.entry(id).and_modify(|e| {
*e=(*e).min(depth);
}).or_insert(depth);
}
for (id,depth) in max {
hm_max.entry(id).and_modify(|e| {
*e=(*e).max(depth);
}).or_insert(depth);
}
}
println!("Final: {:?}",(hm_min.len(),hm_max.len()));
let mut fh_max = BufWriter::new(File::create("route_log_max.txt").unwrap());
for (id,depth) in hm_max {
write!(fh_max,"{},{}\n",id,depth).unwrap();
}
let mut fh_min = BufWriter::new(File::create("route_log_min.txt").unwrap());
for (id,depth) in hm_min {
write!(fh_min,"{},{}\n",id,depth).unwrap();
}
}

View File

@ -0,0 +1,79 @@
import sys
import datashader as ds
import pandas as pd
import datashader.transfer_functions as tf
from datashader.utils import export_image
from datashader.transfer_functions import set_background
import subprocess as SP
import os
import itertools as ITT
from glob import glob
print("Loading stars...")
stars = pd.read_csv("stars.csv", usecols=["id", "x", "z", "mult"], index_col=0)
stars.loc[stars.mult == 1.0, "mult"] = float("nan")
steps = int(sys.argv[1])
size = 1080
mode = "eq_hist"
cvs = ds.Canvas(plot_width=size, plot_height=size)
print("Plotting density")
density_agg = cvs.points(stars, "x", "z")
density = tf.shade(density_agg, cmap=["black", "white"], how=mode)
print("Plotting neutrons")
neutrons_agg = cvs.points(stars, "x", "z", agg=ds.count("mult"))
neutrons = tf.shade(neutrons_agg, cmap=["darkblue", "lightblue"], how=mode)
base = tf.stack(density, neutrons)
# ffplay = SP.Popen([
# "ffplay","-f","image2pipe","-"
# ],stdin=SP.PIPE,bufsize=0)
for rh_fn in ITT.chain.from_iterable(map(glob, sys.argv[2:])):
basename = os.path.splitext(os.path.split(rh_fn)[-1])[0]
filename = "img/{}_{}_{}.mkv".format(basename, size, mode)
ffmpeg = SP.Popen(
[
"ffmpeg",
"-y",
"-f",
"image2pipe",
"-i",
"-",
"-crf",
"17",
"-r",
"25",
"-pix_fmt",
"yuv420p",
filename,
],
stdin=SP.PIPE,
bufsize=0,
)
print("Loading", rh_fn)
route_hist = pd.read_csv(
rh_fn, names=["id", "d"], index_col=0, dtype={"d": int}, low_memory=False,
)
exp_span = [route_hist.d.min(), route_hist.d.max()]
stars["d"] = float("nan")
rng = range(route_hist.d.min(), route_hist.d.max() + 1, steps)
if steps == 0:
rng = [route_hist.d.max() + 1]
for n in rng:
stars['d'] = route_hist[route_hist.d < n] # slow
explored_agg = cvs.points(stars, "x", "z", agg=ds.mean("d")) # slow
explored = tf.shade(
explored_agg, cmap=["darkred", "lightpink"], how="linear", span=exp_span
)
img = set_background(tf.stack(base, explored), "black").to_pil()
img.save(ffmpeg.stdin, "png")
img.save("img/current.png")
ffmpeg.stdin.close()
ffmpeg.wait()

View File

@ -0,0 +1,79 @@
import pandas as pd
import vaex as vx
from PIL import Image, ImageDraw, ImageFont
from skimage import exposure
from skimage.util import img_as_ubyte
import numpy as np
from matplotlib import cm
import sys
base_size = 1080, 1920
def scale_to(width=None, height=None):
isnone = (width is None, height is None)
ret = {
(False, False): lambda w, h: (w, h),
(True, True): lambda w, h: (width, height),
(False, True): lambda w, h: (width, width * (h / w)),
(True, False): lambda w, h: (height * (w / h), height),
}
return lambda *args: tuple(map(int, ret[isnone](*args)))
# xz -1 1
bining = {
("zx", -1, 1): scale_to(width=base_size[0]), # main view, top down
# ('yx',1,1): lambda size,w,h: (size,int(size*(w/h))), #
# ('zy',-1,1): lambda size,w,h: (int(size*(h/w)),size), #
}
print("Loading stars.csv")
stars = pd.read_csv(
"stars.csv",
names=["id", "name", "num_bodies", "has_scoopable", "mult", "x", "y", "z"],
usecols=["id", "num_bodies", "x", "y", "z", "mult"],
index_col=0,
)
stars = vx.from_pandas(stars, copy_index=False)
filename = "heuristic.png"
fnt = ImageFont.truetype(r"FiraCode-Regular", 40)
for (binby_key, m1, m2), calcshape in bining.items():
binby = [m1 * stars[binby_key[0]], m2 * stars[binby_key[1]]]
mm = [binby[0].minmax(), binby[1].minmax()]
w, h = [mm[0][1] - mm[0][0], mm[1][1] - mm[1][0]]
shape = calcshape(w, h)
hm_all = stars.sum("num_bodies", binby=binby, shape=shape, limits="minmax")
hm_all_mask = hm_all != 0
hm_all = exposure.equalize_hist(hm_all)
hm_all -= hm_all.min()
hm_all /= hm_all.max()
hm_boost = stars.sum(
"astype(mult>1.0,'int')", binby=binby, shape=shape, limits="minmax"
)
hm_boost_mask = hm_boost != 0
hm_boost = exposure.equalize_hist(hm_boost)
hm_boost -= hm_boost.min()
hm_boost /= hm_boost.max()
# R = cm.Reds_r()
G = cm.Greens_r(hm_all)
B = cm.Blues_r(hm_boost)
img = np.zeros((base_size[0], base_size[1], 4))
img[:, :, :] = 0.0
img[:, :, 3] = 1.0
canvas = img[: shape[0], : shape[1], :]
canvas[hm_all_mask] = G[hm_all_mask]
canvas[hm_boost_mask] = B[hm_boost_mask]
pil_img = Image.fromarray(img_as_ubyte(img))
draw = ImageDraw.Draw(pil_img)
messages = ["Hello World"]
draw.multiline_text((shape[0], 0), "\n".join(messages), font=fnt)
pil_img.save(filename)

208
render_heatmap_vid_vaex.py Normal file
View File

@ -0,0 +1,208 @@
import pandas as pd
import vaex as vx
import json
from PIL import Image, ImageDraw, ImageFont
from skimage import exposure
from skimage.io import imsave
from skimage.util import img_as_ubyte
import numpy as np
from matplotlib import cm
import subprocess as SP
import os
import sys
import gc
from datetime import timedelta
import itertools as ITT
from glob import glob
base_size = 1080, 1920
steps = 1
framerate = 25
rh_fn = sys.argv[1]
def scale_to(width=None, height=None):
isnone = (width is None, height is None)
ret = {
(False, False): lambda w, h: (w, h),
(True, True): lambda w, h: (width, height),
(False, True): lambda w, h: (width, width * (h / w)),
(True, False): lambda w, h: (height * (w / h), height),
}
return lambda *args: tuple(map(int, ret[isnone](*args)))
# xz -1 1
bining = {
("zx", -1, 1): scale_to(width=base_size[0]), # main view, top down
# ('yx',1,1): lambda size,w,h: (size,int(size*(w/h))), #
# ('zy',-1,1): lambda size,w,h: (int(size*(h/w)),size), #
}
def apply_depth(stars, rh_fn):
print("Loading", rh_fn, flush=True, end=" ")
route_hist = pd.read_csv(
rh_fn,
names=["id", "depth"],
index_col=0,
dtype={"depth": int},
low_memory=False,
)
print("OK")
print("Converting to pandas dataframe", flush=True, end=" ")
stars = stars.to_pandas_df()
gc.collect()
print("OK")
print("Applying depth", flush=True, end=" ")
stars["depth"] = float("nan")
print("...",flush=True,end=" ")
stars["depth"] = route_hist.depth + 1.0
print("OK")
print("Converting to vaex dataframe", flush=True, end=" ")
stars = vx.from_pandas(stars, copy_index=False)
gc.collect()
print("OK")
return stars, route_hist.depth.max()
"""
#[derive(Debug, Clone, Serialize, Deserialize, IntoPyObject)]
pub struct System {
/// Unique System id
pub id: u32,
/// Star system
pub name: String,
/// Number of bodies
pub num_bodies: u8,
/// Does the system have a scoopable star?
pub has_scoopable: bool,
/// Jump range multiplier (1.5 for white dwarfs, 4.0 for neutron stars, 1.0 otherwise)
pub mult: f32,
/// Position
pub pos: [f32; 3],
}
"""
print("Loading stars.csv")
stars = pd.read_csv(
"stars.csv",
names=["id", "name", "num_bodies", "has_scoopable", "mult", "x", "y", "z"],
usecols=["id", "num_bodies", "x", "y", "z", "mult"],
index_col=0,
)
stars = vx.from_pandas(stars, copy_index=False)
def render(stars, rh_fn):
print("Rendering")
json_file = os.path.splitext(rh_fn)[0] + ".json"
if os.path.isfile(json_file):
with open(json_file) as fh:
route_info = json.load(fh)
route_len = len(route_info["route"])
time_taken = str(timedelta(seconds=route_info["dt"]))
route_rate = route_len / route_info["dt"]
else:
time_taken = "N/A"
route_len = 0
route_rate = 0
route_info = {"dt": -1.0}
stars, d_max = apply_depth(stars, rh_fn)
basename = os.path.splitext(os.path.split(rh_fn)[-1])[0]
filename = "img/{}.mkv".format(basename)
if os.path.isfile(filename):
return
ffmpeg = SP.Popen(
[
"ffmpeg",
"-y",
"-f",
"image2pipe",
"-probesize",
"128M",
"-i",
"-",
"-crf",
"17",
"-preset",
"veryslow",
"-r",
str(framerate),
"-pix_fmt",
"yuv420p",
filename,
],
stdin=SP.PIPE,
bufsize=0,
)
total = stars.length()
fnt = ImageFont.truetype(r"FiraCode-Regular", 40)
for (binby_key, m1, m2), calcshape in bining.items():
binby = [m1 * stars[binby_key[0]], m2 * stars[binby_key[1]]]
mm = [binby[0].minmax(), binby[1].minmax()]
w, h = [mm[0][1] - mm[0][0], mm[1][1] - mm[1][0]]
shape = calcshape(w, h)
hm_all = stars.sum("num_bodies", binby=binby, shape=shape, limits="minmax")
hm_all_mask = hm_all != 0
hm_all = exposure.equalize_hist(hm_all)
hm_all -= hm_all.min()
hm_all /= hm_all.max()
hm_boost = stars.sum(
"astype(mult>1.0,'int')", binby=binby, shape=shape, limits="minmax"
)
hm_boost_mask = hm_boost != 0
hm_boost = exposure.equalize_hist(hm_boost)
hm_boost -= hm_boost.min()
hm_boost /= hm_boost.max()
G = cm.Greens_r(hm_all)
B = cm.Blues_r(hm_boost)
hm_exp = stars.mean("depth", binby=binby, shape=shape, limits="minmax")
hm_exp[np.isnan(hm_exp)] = 0.0
hm_exp -= hm_exp.min()
hm_exp /= d_max
R = cm.Reds_r(hm_exp)
hm_exp_mask_base = hm_exp != 0.0
img = np.zeros((base_size[0], base_size[1], 4))
d_array = stars[~stars["depth"].isna()]["depth"].values
exploration_rate = (d_array <= d_max).sum() / route_info["dt"]
print("Total frames:",d_max)
for d in range(0, d_max, steps):
hm_exp_mask = np.logical_and(hm_exp_mask_base, hm_exp <= (d / d_max))
num_explored = (d_array <= d).sum()
img[:, :, :] = 0.0
img[:, :, 3] = 1.0
canvas = img[: shape[0], : shape[1], :]
canvas[hm_all_mask] = G[hm_all_mask]
canvas[hm_boost_mask] = B[hm_boost_mask]
canvas[hm_exp_mask] = R[hm_exp_mask]
pil_img = Image.fromarray(img_as_ubyte(img))
draw = ImageDraw.Draw(pil_img)
messages = [
"Filename: {}".format(basename),
"Total Stars: {:,}".format(total),
"Explored: {:,} ({:.2%})".format(num_explored, num_explored / total),
"Search Depth: {:,}/{:,}".format(d, route_len),
"Time: {}".format(time_taken),
"Rate: {:.3f} waypoints/s".format(route_rate),
"Exploration Rate: {:.3f} stars/s".format(exploration_rate),
]
draw.multiline_text((shape[0], 0), "\n".join(messages), font=fnt)
pil_img.save(ffmpeg.stdin, "bmp")
ffmpeg.stdin.close()
ffmpeg.wait()
for rh_fn in ITT.chain.from_iterable(map(glob, sys.argv[1:])):
render(stars, rh_fn)

5356
route_log_max.txt Normal file

File diff suppressed because it is too large Load Diff

5356
route_log_min.txt Normal file

File diff suppressed because it is too large Load Diff

19
rust/.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,19 @@
{
"spellright.language": [
"de",
"en"
],
"spellright.documentTypes": [
"latex",
"plaintext",
"git-commit"
],
"discord.enabled": true,
"python.pythonPath": "..\\.nox\\devenv-3-8\\python.exe",
"jupyter.jupyterServerType": "remote",
"files.associations": {
"*.ksy": "yaml",
"*.vpy": "python",
"stat.h": "c"
}
}

1660
rust/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,42 +1,79 @@
[package]
name = "ed_lrr"
version = "0.2.0"
authors = [ "Daniel Seiller <earthnuker@gmail.com>",]
edition = "2018"
repository = "https://gitlab.com/Earthnuker/ed_lrr.git"
license = "MIT"
[lib]
crate-type = [ "cdylib",]
name = "_ed_lrr"
[dependencies]
csv = "1.1.3"
humantime = "2.0.1"
permutohedron = "0.2.4"
serde_json = "1.0.55"
fnv = "1.0.7"
bincode = "1.2.1"
sha3 = "0.9.0"
byteorder = "1.3.4"
strsim = "0.10.0"
rstar = "0.8.0"
crossbeam-channel = "0.4.2"
better-panic = "0.2.0"
derivative = "2.1.1"
dict_derive = "0.2.0"
num_cpus = "1.13.0"
regex = "1.3.9"
chrono = "0.4.11"
[dependencies.pyo3]
git = "https://github.com/PyO3/pyo3"
features = [ "extension-module",]
[dependencies.serde]
version = "1.0.112"
features = [ "derive",]
[profile.release]
codegen-units = 1
lto = true
[package]
name = "ed_lrr"
version = "0.2.0"
authors = ["Daniel Seiller <earthnuker@gmail.com>"]
edition = "2018"
repository = "https://gitlab.com/Earthnuker/ed_lrr.git"
license = "MIT"
[lib]
crate-type = ["cdylib"]
name = "_ed_lrr"
[profile.release]
codegen-units = 1
opt-level = 3
debug = true
lto = "fat"
[dependencies]
pyo3 = { version = "0.15.1", features = ["extension-module","eyre"] }
csv = "1.1.6"
humantime = "2.1.0"
permutohedron = "0.2.4"
serde_json = "1.0.74"
fnv = "1.0.7"
bincode = "1.3.3"
sha3 = "0.10.0"
byteorder = "1.4.3"
rstar = "0.9.2"
crossbeam-channel = "0.5.2"
better-panic = "0.3.0"
derivative = "2.2.0"
dict_derive = "0.4.0"
regex = "1.5.4"
num_cpus = "1.13.1"
eddie = "0.4.2"
thiserror = "1.0.30"
pyo3-log = "0.5.0"
log = "0.4.14"
flate2 = "1.0.22"
eval = "0.4.3"
pythonize = "0.15.0"
itertools = "0.10.3"
intmap = "0.7.1"
diff-struct = "0.4.1"
rustc-hash = "1.1.0"
stats_alloc = "0.1.8"
tracing = { version = "0.1.29", optional = true }
tracing-subscriber = { version = "0.3.5", optional = true }
tracing-tracy = { version = "0.8.0", optional = true }
tracing-unwrap = { version = "0.9.2", optional = true }
tracy-client = { version = "0.12.6", optional = true }
tracing-chrome = "0.4.0"
rand = "0.8.4"
eyre = "0.6.6"
memmap = "0.7.0"
csv-core = "0.1.10"
postcard = { version = "0.7.3", features = ["alloc"] }
nohash-hasher = "0.2.0"
[features]
profiling = ["tracing","tracing-subscriber","tracing-tracy","tracing-unwrap","tracy-client"]
[dev-dependencies]
criterion = { version = "0.3.5", features = ["real_blackbox"] }
rand = "0.8.4"
rand_distr = "0.4.2"
[dependencies.serde]
version = "1.0.133"
features = ["derive"]
[[bench]]
name = "dot_bench"
harness = false

29
rust/analyze_logs.py Normal file
View File

@ -0,0 +1,29 @@
import ujson
from glob import glob
import pandas as pd
from datetime import timedelta
route_info = {}
for log in glob("../logs/route_log*.json"):
name = log.split("route_log_")[1].rsplit(".", 1)[0]
data = ujson.load(open(log))
dt = data["dt"]
route_len = len(data["route"])
if route_len:
route_info[name] = (dt, route_len)
dt, route_len = route_info["beam_0"] # BFS as baseline
data = []
for name, (dt_o, l_o) in sorted(route_info.items(), key=lambda v: v[1][0] / v[1][1]):
dt_s = str(timedelta(seconds=round(dt_o, 2))).rstrip("0")
data.append(
{
"name": name,
"time": "{} ({:.2f}x)".format(dt_s, dt / dt_o),
"length": "{} (+{:.2%})".format(l_o, (l_o / route_len) - 1),
"time/hop": "{:.2} s".format(dt_o / l_o),
}
)
df = pd.DataFrame(data)
print(df.to_markdown(index=False))

142
rust/benches/dot_bench.rs Normal file
View File

@ -0,0 +1,142 @@
use criterion::{criterion_group, criterion_main, BatchSize, Criterion};
use rand::Rng;
use rand_distr::StandardNormal;
fn rand_v3() -> [f32; 3] {
let mut rng = rand::thread_rng();
[
rng.sample(StandardNormal),
rng.sample(StandardNormal),
rng.sample(StandardNormal),
]
}
fn arand() -> f32 {
let mut rng = rand::thread_rng();
rng.sample::<f32, _>(StandardNormal).abs()
}
#[inline(always)]
fn veclen(v: &[f32; 3]) -> f32 {
(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt()
}
#[inline(always)]
fn dist2(p1: &[f32; 3], p2: &[f32; 3]) -> f32 {
let dx = p1[0] - p2[0];
let dy = p1[1] - p2[1];
let dz = p1[2] - p2[2];
dx * dx + dy * dy + dz * dz
}
#[inline(always)]
fn dist(p1: &[f32; 3], p2: &[f32; 3]) -> f32 {
dist2(p1, p2).sqrt()
}
/// Dot product (cosine of angle) between two 3D vectors
#[inline(always)]
pub fn ndot_vec_dist(u: &[f32; 3], v: &[f32; 3]) -> f32 {
let z: [f32; 3] = [0.0; 3];
let lm = dist(u, &z) * dist(v, &z);
((u[0] * v[0]) + (u[1] * v[1]) + (u[2] * v[2])) / lm
}
/// Dot product (cosine of angle) between two 3D vectors
#[inline(always)]
pub fn ndot_vec_len(u: &[f32; 3], v: &[f32; 3]) -> f32 {
let lm = veclen(u) * veclen(v);
((u[0] * v[0]) + (u[1] * v[1]) + (u[2] * v[2])) / lm
}
#[inline(always)]
pub fn ndot_iter(u: &[f32; 3], v: &[f32; 3]) -> f32 {
let mut l_u = 0.0;
let mut l_v = 0.0;
let mut l_s = 0.0;
for (u, v) in u.iter().zip(v.iter()) {
l_s += u * v;
l_u += u * u;
l_v += v * v;
}
l_s / (l_u * l_v).sqrt()
}
fn bench_ndot(c: &mut Criterion) {
let mut g = c.benchmark_group("ndot");
g.bench_function("vec_dist", |b| {
b.iter_batched(
|| (rand_v3(), rand_v3()),
|(v1, v2)| ndot_vec_dist(&v1, &v2),
BatchSize::SmallInput,
);
});
g.bench_function("vec_len", |b| {
b.iter_batched(
|| (rand_v3(), rand_v3()),
|(v1, v2)| ndot_vec_len(&v1, &v2),
BatchSize::SmallInput,
);
});
g.bench_function("iter", |b| {
b.iter_batched(
|| (rand_v3(), rand_v3()),
|(v1, v2)| ndot_iter(&v1, &v2),
BatchSize::SmallInput,
);
});
g.finish();
}
fn bench_dist(c: &mut Criterion) {
let mut g = c.benchmark_group("dist");
g.bench_function("dist", |b| {
b.iter_batched(
|| (rand_v3(), rand_v3()),
|(v1, v2)| dist(&v1, &v2),
BatchSize::SmallInput,
);
});
g.bench_function("dist2", |b| {
b.iter_batched(
|| (rand_v3(), rand_v3()),
|(v1, v2)| dist2(&v1, &v2),
BatchSize::SmallInput,
);
});
g.finish();
}
fn vsub(a: &[f32; 3], b: &[f32; 3]) -> [f32; 3] {
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
pub fn h_old(node: &[f32; 3], m: f32, goal: &[f32; 3], r: f32) -> f32 {
(dist(node, goal) - (r * m)).max(0.0)
}
pub fn h_new(node: &[f32; 3], next: &[f32; 3], goal: &[f32; 3]) -> f32 {
-ndot_iter(&vsub(node, goal), &vsub(node, next)).acos()
}
fn bench_new_heur(c: &mut Criterion) {
c.bench_function("old_heuristic", |b| {
b.iter_batched(
|| (rand_v3(), arand(), rand_v3(), arand()),
|(node, m, goal, range)| h_old(&node, m, &goal, range),
BatchSize::SmallInput,
);
});
c.bench_function("new_heuristic", |b| {
b.iter_batched(
|| (rand_v3(), rand_v3(), rand_v3()),
|(v1, v2, v3)| h_new(&v1, &v2, &v3),
BatchSize::SmallInput,
);
});
}
criterion_group!(benches, bench_ndot, bench_dist, bench_new_heur);
criterion_main!(benches);

File diff suppressed because it is too large Load Diff

0
rust/deps.svg Normal file
View File

46
rust/multi_test.py Normal file
View File

@ -0,0 +1,46 @@
import os
def setup_logging(loglevel="INFO"):
import logging
import coloredlogs
import datetime
coloredlogs.DEFAULT_FIELD_STYLES["delta"] = {"color": "green"}
coloredlogs.DEFAULT_FIELD_STYLES["levelname"] = {"color": "yellow"}
class DeltaTimeFormatter(coloredlogs.ColoredFormatter):
def format(self, record):
seconds = record.relativeCreated / 1000
duration = datetime.timedelta(seconds=seconds)
record.delta = str(duration)
return super().format(record)
coloredlogs.ColoredFormatter = DeltaTimeFormatter
logfmt = " | ".join(
["[%(delta)s] %(levelname)s", "%(name)s:%(pathname)s:%(lineno)s", "%(message)s"]
)
numeric_level = getattr(logging, loglevel.upper(), None)
if not isinstance(numeric_level, int):
raise ValueError("Invalid log level: %s" % loglevel)
coloredlogs.install(level=numeric_level, fmt=logfmt)
setup_logging()
_ed_lrr = __import__("_ed_lrr")
r = _ed_lrr.PyRouter(None)
r.load("stars.csv")
# r.run_bfs(48)
r.test(48)
exit()
_ed_lrr.PyRouter.preprocess_galaxy("E:/EDSM/galaxy.json.gz", "E:/EDSM/stars.csv")
exit()
r = _ed_lrr.PyRouter(print)
r.load("../stars.csv")
systems = r.resolve_systems((0, 0, 0), "Colonia", 18627)
print(systems)
print(systems[0, 0, 0])

0
rust/route_log.txt Normal file
View File

221
rust/run_test.py Normal file
View File

@ -0,0 +1,221 @@
import subprocess as SP
import sys
from datetime import datetime, timedelta
import os
import shutil
import json
def setup_logging(loglevel="INFO"):
import logging
import coloredlogs
coloredlogs.DEFAULT_FIELD_STYLES["delta"] = {"color": "green"}
coloredlogs.DEFAULT_FIELD_STYLES["levelname"] = {"color": "yellow"}
class DeltaTimeFormatter(coloredlogs.ColoredFormatter):
def format(self, record):
seconds = record.relativeCreated / 1000
duration = timedelta(seconds=seconds)
record.delta = str(duration)
return super().format(record)
coloredlogs.ColoredFormatter = DeltaTimeFormatter
logfmt = " | ".join(
["[%(delta)s] %(levelname)s", "%(name)s:%(pathname)s:%(lineno)s", "%(message)s"]
)
numeric_level = getattr(logging, loglevel.upper(), None)
if not isinstance(numeric_level, int):
raise ValueError("Invalid log level: %s" % loglevel)
coloredlogs.install(level=numeric_level, fmt=logfmt)
setup_logging()
JUMP_RANGE = 48
globals().setdefault("__file__", r"D:\devel\rust\ed_lrr_gui\rust\run_test.py")
dirname = os.path.dirname(__file__) or "."
os.chdir(dirname)
t_start = datetime.now()
os.environ["PYO3_PYTHON"] = sys.executable
if "--clean" in sys.argv[1:]:
SP.check_call(["cargo","clean"])
if "--build" in sys.argv[1:]:
SP.check_call(["cargo","lcheck"])
SP.check_call([sys.executable, "-m", "pip", "install", "-e", ".."])
print("Build+Install took:", datetime.now() - t_start)
sys.path.append("..")
_ed_lrr = __import__("_ed_lrr")
def callback(state):
print(state)
print(_ed_lrr)
r = _ed_lrr.PyRouter(callback)
r.load("../stars_2.csv", immediate=False)
print(r)
r.str_tree_test()
exit()
r = _ed_lrr.PyRouter(callback)
r.load("../stars.csv", immediate=False)
print(r.resolve("Sol","Saggitarius A","Colonia","Merope"))
exit()
ships = _ed_lrr.PyShip.from_journal()
r = _ed_lrr.PyRouter(callback)
r.load("../stars.csv", immediate=False)
def func(*args,**kwargs):
print(kwargs)
return 12
r.precompute_neighbors(JUMP_RANGE)
exit()
# start, end = "Sol", "Colonia" # # 135 in 22m 36s 664ms 268us 800ns
"""
{'mode': 'BFS_serial', 'system': 'Nuwo OP-N c23-1', 'from': 'Sol', 'to': 'Beagle Point', 'depth': 492, 'queue_size': 1602, 'd_rem': 2456.31298828125, 'd_total': 65279.3515625, 'prc_done': 96.23722839355469, 'n_seen': 17366296, 'prc_seen': 26.25494384765625}
[0:43:19.715858] INFO | _ed_lrr.route:src\route.rs:2402 | Took: 34m 38s 40ms 256us 500ns
"""
"""
{'mode': 'BFS_serial', 'system': 'Syriae Thaa DN-B d13-2', 'from': 'Sol', 'to': 'Beagle Point', 'depth': 521, 'queue_size': 2311, 'd_rem': 492.8757019042969, 'd_total': 65279.3515625, 'prc_done': 99.2449722290039, 'n_seen': 19566797, 'prc_seen': 29.58173179626465}
[0:53:28.431326] INFO | _ed_lrr.route:src\route.rs:2402 | Took: 48m 34s 958ms 326us 300ns
"""
"""
[0:36:02.738233] INFO | _ed_lrr.route:src\route.rs:2404 | Took: 27m 6s 216ms 161us 100ns
Optimal route: 534
"""
"""
Sol, Colonia
Took: 30m 22s 63ms 818us
Allocs: 26622742
Reallocs: 45809664
Deallocs: 26622600
Optimal route: 135
"""
"""
Sol, Ix
Took: 1s 995ms 115us 100ns
Allocs: 17058
Reallocs: 32042
Deallocs: 17047
Optimal route: 4
"""
# Stats { allocations: 23257531, deallocations: 23257389, reallocations: 42747420, bytes_allocated: 179667997387, bytes_deallocated: 179667853217, bytes_reallocated: 151573742821 }
start, end = "Sol", "Colonia"
systems = r.resolve(start, end)
sys_ids = {k: v["id"] for k, v in systems.items()}
cfg = {}
cfg["mode"] = "incremental_broadening"
# input("{}>".format(os.getpid()))
route = r.route([sys_ids[start], sys_ids[end]], JUMP_RANGE, cfg, 0)
print("Optimal route:", len(route))
# cfg["mode"] = "beam_stack"
# route = r.route([sys_ids[start], sys_ids[end]], JUMP_RANGE, cfg, 0)
exit()
# bw_l = [
# 1,
# 2,
# 4,
# 8,
# 16,
# 32,
# 64,
# 128,
# 256,
# 512,
# 1024,
# 2048,
# 4096,
# 8192,
# 16384,
# 0.1,
# 0.25,
# 0.5,
# 0.75,
# 0.9,
# 0.99,
# 0,
# ]
# cfg = {
# "mode": "bfs",
# "greedyness": 0,
# }
# bw_l = [0]
# for bw in bw_l:
# ofn = "../logs/route_log_beam_{}.txt".format(bw)
# # if os.path.isfile(ofn):
# # continue
# print(ofn)
# t_start = datetime.today()
# try:
# if isinstance(bw, int):
# cfg["beam_width"] = {"absolute": bw}
# else:
# cfg["beam_width"] = {"fraction": bw}
# route = r.route([sys_ids["Sol"], sys_ids["Beagle Point"]], JUMP_RANGE, cfg, 8)
# print(route)
# except Exception as e:
# print("Error:", e)
# route = []
# dt = (datetime.today() - t_start).total_seconds()
# shutil.copy("route_log.txt", ofn)
# with open(ofn.replace(".txt", ".json"), "w") as of:
# json.dump({"route": route, "dt": dt}, of)
# g_l = [1.0, 0.99, 0.9, 0.75, 0.5, 0.25]
# g_l.clear()
# cfg["beam_width"] = 0
# for g in g_l:
# ofn = "../logs/route_log_g_{}.txt".format(g)
# if os.path.isfile(ofn):
# continue
# print(ofn)
# t_start = datetime.today()
# try:
# cfg["greedyness"] = g
# route = r.route([sys_ids["Sol"], sys_ids["Beagle Point"]], JUMP_RANGE, cfg)
# except Exception as e:
# print("Error:", e)
# route = []
# dt = (datetime.today() - t_start).total_seconds()
# shutil.copy("route_log.txt", ofn)
# with open(ofn.replace(".txt", ".json"), "w") as of:
# json.dump({"route": route, "dt": dt}, of)
# r.unload()
# exit()
# os.chdir("..")
# SP.check_call(
# [
# "conda",
# "run",
# "-n",
# "base",
# "--no-capture-output",
# "python",
# "plot_heatmap_vaex.py",
# "logs/route_log_*.txt",
# ],
# shell=True,
# )

View File

@ -1,13 +1,257 @@
use crate::route::Router;
//! # Common utlility functions
use crate::route::{LineCache, Router};
use bincode::Options;
use crossbeam_channel::{bounded, Receiver};
use csv::ByteRecord;
use dict_derive::IntoPyObject;
use pyo3::conversion::ToPyObject;
use eyre::Result;
use log::*;
use pyo3::prelude::*;
use pyo3::types::{PyDict, PyTuple};
use pyo3::types::PyDict;
use pyo3::{conversion::ToPyObject, create_exception};
use pythonize::depythonize;
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
use std::collections::HashMap;
use std::path::PathBuf;
use sha3::{Digest, Sha3_256};
use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
use std::hash::{Hash, Hasher, BuildHasherDefault};
use std::io::Write;
use std::ops::{Deref, DerefMut};
use std::path::Path;
use std::str::FromStr;
use std::thread;
use std::{cmp::Ordering, cmp::Reverse, collections::BinaryHeap};
use std::{
fs::File,
io::{BufReader, BufWriter},
path::PathBuf,
};
use nohash_hasher::NoHashHasher;
use thiserror::Error;
#[inline(always)]
pub fn heuristic(range: f32, node: &TreeNode, goal: &TreeNode) -> f32 {
// distance remaining after jumping from node towards goal
let a2 = dist2(&node.pos, &goal.pos);
let mult=node.get_mult();
let b2 = range * range * mult*mult;
return (a2 - b2).max(0.0);
}
/// Min-heap priority queue using f32 as priority
pub struct MinFHeap<T: Ord>(pub BinaryHeap<(Reverse<F32>, T)>);
/// Max-heap priority queue using f32 as priority
pub struct MaxFHeap<T: Ord>(pub BinaryHeap<(F32, T)>);
impl<T: Ord> MaxFHeap<T> {
/// Create new, empty priority queue
pub fn new() -> Self {
MaxFHeap(BinaryHeap::new())
}
/// push value `item` with priority `w` into queue
pub fn push(&mut self, w: f32, item: T) {
self.0.push((F32(w), item))
}
/// Remove and return largest item and priority
pub fn pop(&mut self) -> Option<(f32, T)> {
self.0.pop().map(|(F32(w), item)| (w, item))
}
}
impl<T: Ord> Default for MaxFHeap<T> {
fn default() -> Self {
return MaxFHeap(BinaryHeap::new());
}
}
impl<T: Ord> Deref for MaxFHeap<T> {
type Target = BinaryHeap<(F32, T)>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<T: Ord> DerefMut for MaxFHeap<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl<T: Ord> MinFHeap<T> {
/// Create new, empty priority queue
pub fn new() -> Self {
MinFHeap(BinaryHeap::new())
}
/// push value `item` with priority `w` into queue
pub fn push(&mut self, w: f32, item: T) {
self.0.push((Reverse(F32(w)), item))
}
/// Remove and return smallest item and priority
pub fn pop(&mut self) -> Option<(f32, T)> {
self.0.pop().map(|(Reverse(F32(w)), item)| (w, item))
}
}
impl<T: Ord> Default for MinFHeap<T> {
fn default() -> Self {
return MinFHeap(BinaryHeap::new());
}
}
impl<T: Ord> Deref for MinFHeap<T> {
type Target = BinaryHeap<(Reverse<F32>, T)>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<T: Ord> DerefMut for MinFHeap<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
/// ED LRR error type
#[derive(Error, Debug)]
pub enum EdLrrError {
#[error("failed to compute route from {from:?} to {to:?}: {reason}")]
RouteError {
from: Option<System>,
to: Option<System>,
reason: String,
},
#[error("failed to find system matching {0:?}")]
ResolveError(String),
#[error("runtime error: {0:?}")]
RuntimeError(String),
#[error("Failed to process {0}")]
ProcessingError(PathBuf),
#[error(transparent)]
EvalError(#[from] eval::Error),
#[error(transparent)]
CSVError(#[from] csv::Error),
#[error(transparent)]
IOError(#[from] std::io::Error),
#[error(transparent)]
BincodeError(#[from] Box<bincode::ErrorKind>),
#[error(transparent)]
PyError(#[from] pyo3::PyErr),
#[error(transparent)]
Error(#[from] eyre::Error),
#[error("unknown error")]
Unknown,
}
pub mod py_exceptions {
use super::*;
pub use pyo3::exceptions::*;
create_exception!(_ed_lrr, RouteError, PyException);
create_exception!(_ed_lrr, ResolveError, PyException);
create_exception!(_ed_lrr, EdLrrException, PyException);
create_exception!(_ed_lrr, ProcessingError, PyException);
create_exception!(_ed_lrr, FileFormatError, PyException);
}
impl FromStr for EdLrrError {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self::RuntimeError(s.to_owned()))
}
}
impl std::convert::From<String> for EdLrrError {
fn from(s: String) -> Self {
Self::RuntimeError(s)
}
}
impl std::convert::From<EdLrrError> for PyErr {
fn from(err: EdLrrError) -> PyErr {
match err {
EdLrrError::PyError(e) => e,
EdLrrError::BincodeError(..) => {
py_exceptions::FileFormatError::new_err(err.to_string())
}
EdLrrError::RouteError { .. } => py_exceptions::RouteError::new_err(err.to_string()),
EdLrrError::RuntimeError(msg) => py_exceptions::PyRuntimeError::new_err(msg),
EdLrrError::ResolveError(..) => py_exceptions::PyRuntimeError::new_err(err.to_string()),
EdLrrError::EvalError(err) => py_exceptions::PyRuntimeError::new_err(err.to_string()),
EdLrrError::CSVError(err) => py_exceptions::PyRuntimeError::new_err(err.to_string()),
EdLrrError::IOError(err) => py_exceptions::PyIOError::new_err(err.to_string()),
EdLrrError::Error(err) => py_exceptions::EdLrrException::new_err(err.to_string()),
EdLrrError::ProcessingError(buf) => {
py_exceptions::ProcessingError::new_err(format!("{}", buf.display()))
}
EdLrrError::Unknown => {
py_exceptions::EdLrrException::new_err("Unknown error!".to_string())
}
}
}
}
pub type EdLrrResult<T> = Result<T, EdLrrError>;
/// f32 compare wrapper
pub fn fcmp(a: f32, b: f32) -> Ordering {
match (a, b) {
(x, y) if x.is_nan() && y.is_nan() => Ordering::Equal,
(x, _) if x.is_nan() => Ordering::Greater,
(_, y) if y.is_nan() => Ordering::Less,
(..) => a.partial_cmp(&b).unwrap(),
}
}
/// f32 warpper type implementing `Eq` and `Ord`
#[derive(Debug)]
pub struct F32(pub f32);
impl PartialEq for F32 {
fn eq(&self, other: &F32) -> bool {
fcmp(self.0, other.0) == std::cmp::Ordering::Equal
}
}
impl Eq for F32 {}
impl PartialOrd for F32 {
fn partial_cmp(&self, other: &F32) -> Option<std::cmp::Ordering> {
Some(fcmp(self.0, other.0))
}
}
impl Ord for F32 {
fn cmp(&self, other: &F32) -> std::cmp::Ordering {
fcmp(self.0, other.0)
}
}
impl Deref for F32 {
type Target = f32;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for F32 {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
/// Returns additional jump range (in Ly) granted by specified class of Guardian FSD Booster
pub fn get_fsd_booster_info(class: usize) -> Result<f32, String> {
// Data from https://elite-dangerous.fandom.com/wiki/Guardian_Frame_Shift_Drive_Booster
let ret = match class {
@ -22,6 +266,7 @@ pub fn get_fsd_booster_info(class: usize) -> Result<f32, String> {
return Ok(ret);
}
/// Returns optimal mass and maximum fuel per jump for the given FSD rating and class as a hash map
pub fn get_fsd_info(rating: usize, class: usize) -> Result<HashMap<String, f32>, String> {
let mut ret = HashMap::new();
// Data from https://elite-dangerous.fandom.com/wiki/Frame_Shift_Drive#Specifications
@ -68,6 +313,7 @@ pub fn get_fsd_info(rating: usize, class: usize) -> Result<HashMap<String, f32>,
return Ok(ret);
}
/// Returns jump range multiplier for the specified star type (4 for neutron stars, 1.5 for white dwarfs and 1.0 otherwise)
pub fn get_mult(star_type: &str) -> f32 {
if star_type.contains("White Dwarf") {
return 1.5;
@ -78,133 +324,403 @@ pub fn get_mult(star_type: &str) -> f32 {
1.0
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum BeamWidth {
Absolute(usize),
Fraction(f32),
Radius(f32),
Infinite,
}
impl std::fmt::Display for BeamWidth {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
BeamWidth::Absolute(n) => write!(f, "{}", n),
BeamWidth::Fraction(v) => write!(f, "{}%", (*v) * 100.0),
BeamWidth::Radius(r) => write!(f, "{} Ly", r),
BeamWidth::Infinite => write!(f, "Infinite"),
}?;
Ok(())
}
}
impl Default for BeamWidth {
fn default() -> Self {
Self::Infinite
}
}
impl FromPyObject<'_> for BeamWidth {
fn extract(ob: &PyAny) -> PyResult<Self> {
depythonize(ob).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{}", e)))
}
}
impl BeamWidth {
pub fn is_set(&self) -> bool {
match self {
Self::Fraction(f) => *f > 0.0,
Self::Absolute(n) => *n != 0,
Self::Radius(r) => *r > 0.0,
Self::Infinite => false,
}
}
pub fn is_infinite(&self) -> bool {
matches!(self, Self::Infinite)
}
pub fn compute(&self, nodes: usize) -> usize {
match self {
Self::Fraction(f) => {
let w = (nodes as f32) * f.max(0.0).min(1.0);
return (w.ceil() as usize).max(1);
}
Self::Absolute(n) => *n,
Self::Radius(_) | Self::Infinite => nodes,
}
}
}
/// Represents an uresolved system to be searched for by name, id or position
#[derive(Debug, FromPyObject)]
pub enum SysEntry {
ID(u32),
Name(String),
Pos((f32, f32, f32)),
}
impl SysEntry {
pub fn parse(s: &str) -> Self {
if let Ok(n) = s.parse() {
SysEntry::ID(n)
} else {
SysEntry::Name(s.to_owned())
impl ToPyObject for SysEntry {
fn to_object(&self, py: Python) -> PyObject {
match self {
Self::ID(id) => id.to_object(py),
Self::Name(name) => name.to_object(py),
Self::Pos(pos) => pos.to_object(py),
}
}
}
pub fn find_matches(
path: &PathBuf,
pub fn grid_stats(
path: &Path,
grid_size: f32,
) -> Result<BTreeMap<(i64, i64, i64), Vec<u32>>, String> {
let mut reader = match csv::ReaderBuilder::new().has_headers(false).from_path(path) {
Ok(rdr) => rdr,
Err(e) => {
return Err(format!("Error opening {}: {}", path.to_str().unwrap(), e));
}
};
let systems = reader.deserialize::<System>().map(Result::unwrap);
let mut ret: BTreeMap<(i64, i64, i64), Vec<u32>> = BTreeMap::new();
for sys in systems {
let k = (
((sys.pos[0] / grid_size).round() * grid_size) as i64,
((sys.pos[1] / grid_size).round() * grid_size) as i64,
((sys.pos[2] / grid_size).round() * grid_size) as i64,
);
ret.entry(k).or_default().push(sys.id);
}
Ok(ret)
}
pub enum Node {
Start,
Goal,
ID(u32),
}
pub enum Weight {
Dist(Node),
Depth,
}
impl Weight {
fn eval(&self) -> f32 {
todo!()
}
}
struct Weights(Vec<(f32, Weight)>);
impl Weights {
fn new() -> Self {
Self(vec![])
}
fn add(&mut self, w: f32, v: Weight) {
self.0.push((w, v));
}
fn eval(&mut self) -> f32 {
self.0.iter().map(|(w, v)| w * v.eval()).sum()
}
}
#[inline(always)]
pub fn dist2(p1: &[f32; 3], p2: &[f32; 3]) -> f32 {
let dx = p1[0] - p2[0];
let dy = p1[1] - p2[1];
let dz = p1[2] - p2[2];
dx * dx + dy * dy + dz * dz
}
#[inline(always)]
pub fn dist(p1: &[f32; 3], p2: &[f32; 3]) -> f32 {
dist2(p1, p2).sqrt()
}
#[inline(always)]
pub fn distm(p1: &[f32; 3], p2: &[f32; 3]) -> f32 {
let dx = (p1[0] - p2[0]).abs();
let dy = (p1[1] - p2[1]).abs();
let dz = (p1[2] - p2[2]).abs();
dx + dy + dz
}
/// Dot product (cosine of angle) between two 3D vectors
#[inline(always)]
pub fn ndot(u: &[f32; 3], v: &[f32; 3]) -> f32 {
let z: [f32; 3] = [0.0; 3];
let lm = dist(u, &z) * dist(v, &z);
(u[0] * v[0]) / lm + (u[1] * v[1]) / lm + (u[2] * v[2]) / lm
}
/// Fuzzy string matcher, use to resolve star system names
#[cfg_attr(feature = "profiling", tracing::instrument(skip(rx)))]
fn matcher(
rx: Receiver<ByteRecord>,
names: Vec<String>,
exact: bool,
) -> Result<HashMap<String, (f64, Option<System>)>, String> {
let mut best: HashMap<String, (f64, Option<System>)> = HashMap::new();
) -> HashMap<String, (f64, Option<u32>)> {
let mut best: HashMap<String, (f64, Option<u32>)> = HashMap::new();
for name in &names {
best.insert(name.to_string(), (0.0, None));
}
let names_u8: Vec<(String, _)> = names.iter().map(|n| (n.clone(), n.as_bytes())).collect();
let sdist = eddie::slice::Levenshtein::new();
for sys in rx.into_iter() {
for (name, name_b) in &names_u8 {
if let Some(ent) = best.get_mut(name) {
if (ent.0 - 1.0).abs() < std::f64::EPSILON {
continue;
}
if exact && (&sys[1] == *name_b) {
let id = std::str::from_utf8(&sys[0]).unwrap().parse().unwrap();
*ent = (1.0, Some(id));
continue;
}
let d = sdist.similarity(&sys[1], name_b);
if d > ent.0 {
let id = std::str::from_utf8(&sys[0]).unwrap().parse().unwrap();
*ent = (d, Some(id));
}
};
}
}
best
}
/// Scan through the csv file at `path` and return a hash map
/// mapping the strings from `names` to a tuple `(score, Option<system_id>)`.
/// Scoring matching uses the normalized Levenshtein distance where 1.0 is an exact match.
pub fn find_matches(
path: &Path,
names: Vec<String>,
exact: bool,
) -> Result<HashMap<String, (f64, Option<u32>)>, String> {
let mut best: HashMap<String, (f64, Option<u32>)> = HashMap::new();
if names.is_empty() {
return Ok(best);
}
for name in &names {
best.insert(name.to_string(), (0.0, None));
}
let mut reader = match csv::ReaderBuilder::new().from_path(path) {
let mut workers = Vec::new();
let ncpus = num_cpus::get();
let (tx, rx) = bounded(4096 * ncpus);
for _ in 0..ncpus {
let names = names.clone();
let rx = rx.clone();
let th = thread::spawn(move || matcher(rx, names, exact));
workers.push(th);
}
let mut rdr = match csv::ReaderBuilder::new().has_headers(false).from_path(path) {
Ok(rdr) => rdr,
Err(e) => {
return Err(format!("Error opening {}: {}", path.to_str().unwrap(), e));
}
};
let systems = reader.deserialize::<SystemSerde>();
for sys in systems {
let sys = sys.unwrap();
for name in &names {
let t_start = std::time::Instant::now();
let mut processed: usize = 0;
for record in rdr.byte_records().flat_map(|v| v.ok()) {
tx.send(record).unwrap();
processed += 1;
}
drop(tx);
while let Some(th) = workers.pop() {
for (name, (score, sys)) in th.join().unwrap().iter() {
best.entry(name.clone()).and_modify(|ent| {
if (exact) && (&sys.system == name) {
*ent = (1.0, Some(sys.clone().build()))
} else {
let d1 = strsim::normalized_levenshtein(&sys.system, &name);
let d2 = strsim::normalized_levenshtein(&sys.body, &name);
if d1 > ent.0 {
*ent = (d1, Some(sys.clone().build()))
} else if d2 > ent.0 {
*ent = (d2, Some(sys.clone().build()))
}
if score > &ent.0 {
*ent = (*score, *sys);
}
});
}
}
let dt = std::time::Instant::now() - t_start;
info!(
"Searched {} records in {:?}: {} records/second",
processed,
dt,
(processed as f64) / dt.as_secs_f64()
);
Ok(best)
}
/// Hash the contents of `path` with sha3 and return the hash as a vector of bytes
fn hash_file(path: &Path) -> Vec<u8> {
let mut hash_reader = BufReader::new(File::open(path).unwrap());
let mut hasher = Sha3_256::new();
std::io::copy(&mut hash_reader, &mut hasher).unwrap();
hasher.finalize().iter().copied().collect()
}
/// Construct and `O(1)` lookup index for the csv file at `path`.
/// The structure of the index is `(sha3, Vec<usize>)`
/// where the first element is the sha3 hash of the file the index belongs to
/// followed by a deltified vector where the entry at index `i` is the file offset for line `i` of the csv file.
pub fn build_index(path: &Path) -> std::io::Result<()> {
let file_hash = hash_file(path);
let mut wtr = BufWriter::new(File::create(path.with_extension("idx"))?);
let mut idx: Vec<u8> = Vec::new();
let mut records = (csv::ReaderBuilder::new()
.has_headers(false)
.from_path(path)?)
.into_deserialize::<System>();
let mut n: usize = 0;
let mut size;
idx.push(0);
loop {
n += 1;
if n % 100000 == 0 {
info!("{} Bodies processed", n);
}
let new_pos = records.reader().position().byte();
if records.next().is_none() {
break;
}
size = records.reader().position().byte() - new_pos;
idx.push(size as u8);
}
assert_eq!(idx.len(), n);
bincode::serialize_into(&mut wtr, &(file_hash, idx)).unwrap();
Ok(())
}
/// Node for R*-Tree
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct TreeNode {
/// System ID
pub id: u32,
/// Position in space
pub pos: [f32; 3],
pub mult: f32,
/// flags
/// 00 unscoopable
/// 01 scoopable
/// 10 white dward
/// 11 neutron star
pub flags: u8,
}
impl ToPyObject for TreeNode {
fn to_object(&self, py: Python) -> PyObject {
pythonize::pythonize(py, self).unwrap()
}
}
impl TreeNode {
pub fn get(&self, router: &Router) -> Option<System> {
let mut cache = router.cache.as_ref().unwrap().lock().unwrap();
cache.get(self.id)
/// Retrieve matching [System] for this tree node
pub fn get(&self, router: &Router) -> Result<Option<System>, String> {
router.get(self.id)
}
pub fn get_mult(&self) -> f32 {
match self.flags {
0b11 => 4.0,
0b10 => 1.5,
_ => 1.0
}
}
}
impl PartialEq for TreeNode {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl Eq for TreeNode {}
impl PartialOrd for TreeNode {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.id.cmp(&other.id))
}
}
impl Ord for TreeNode {
fn cmp(&self, other: &TreeNode) -> Ordering {
self.id.cmp(&other.id)
}
}
impl Hash for TreeNode {
fn hash<H: Hasher>(&self, state: &mut H) {
self.id.hash(state);
}
}
/// Star system info read from CSV
#[derive(Debug, Clone, Serialize, Deserialize, IntoPyObject)]
pub struct SystemSerde {
pub id: u32,
pub star_type: String,
pub system: String,
pub body: String,
pub mult: f32,
pub distance: f32,
pub x: f32,
pub y: f32,
pub z: f32,
}
impl SystemSerde {
pub fn build(self) -> System {
System {
id: self.id,
star_type: self.star_type,
system: self.system,
body: self.body,
mult: self.mult,
distance: self.distance,
pos: [self.x, self.y, self.z],
}
}
pub fn to_node(&self) -> TreeNode {
TreeNode {
id: self.id,
pos: [self.x, self.y, self.z],
mult: self.mult,
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct System {
/// Unique System id
pub id: u32,
pub star_type: String,
pub system: String,
pub body: String,
/// Star system
pub name: String,
/// Number of bodies
pub num_bodies: u8,
/// Does the system have a scoopable star?
pub has_scoopable: bool,
/// Jump range multiplier (1.5 for white dwarfs, 4.0 for neutron stars, 1.0 otherwise)
pub mult: f32,
pub distance: f32,
/// Position
pub pos: [f32; 3],
}
impl System {
fn get_flags(&self) -> u8 {
let mut flags=0;
if self.mult==4.0 {
return 0b11
}
if self.mult==1.5 {
return 0b10
}
if self.has_scoopable {
return 0b01
}
return 0b00
}
}
impl ToPyObject for System {
fn to_object(&self, py: Python) -> PyObject {
let pos = PyTuple::new(py, self.pos.iter());
let elem = PyDict::new(py);
elem.set_item("star_type", self.star_type.clone()).unwrap();
elem.set_item("system", self.system.clone()).unwrap();
elem.set_item("body", self.body.clone()).unwrap();
elem.set_item("distance", self.distance).unwrap();
elem.set_item("mult", self.mult).unwrap();
elem.set_item("id", self.id).unwrap();
elem.set_item("pos", pos).unwrap();
elem.to_object(py)
let d = PyDict::new(py);
d.set_item("id", self.id).unwrap();
d.set_item("name", self.name.clone()).unwrap();
d.set_item("num_bodies", self.num_bodies).unwrap();
d.set_item("has_scoopable", self.has_scoopable).unwrap();
d.set_item("mult", self.mult).unwrap();
d.set_item("pos", (self.pos[0], self.pos[1], self.pos[2]))
.unwrap();
return d.to_object(py);
}
}
@ -213,7 +729,7 @@ impl System {
TreeNode {
id: self.id,
pos: self.pos,
mult: self.mult,
flags: self.get_flags(),
}
}
}
@ -229,3 +745,118 @@ impl PartialOrd for System {
Some(self.cmp(other))
}
}
#[derive(Debug)]
pub struct DQueue<T>(Vec<VecDeque<T>>);
impl<T> DQueue<T> {
pub fn new() -> Self {
Self(vec![])
}
pub fn enqueue(&mut self, depth: usize, item: T) {
self.0.resize_with(depth, VecDeque::new);
self.0[depth].push_back(item);
}
pub fn dequeue(&mut self, depth: usize) -> Option<T> {
self.0.resize_with(depth, VecDeque::new);
self.0[depth].pop_back()
}
}
impl<T> Default for DQueue<T> {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Default, Serialize, Deserialize)]
struct BKTreeNode {
ids: HashSet<u32,BuildHasherDefault<NoHashHasher<u32>>>,
children: HashMap<u8,Self,BuildHasherDefault<NoHashHasher<u8>>>
}
impl BKTreeNode {
fn new(data: &[String], dist: &eddie::str::Levenshtein) -> Self {
let mut tree= Self::default();
let mut max_depth=0;
(0..data.len()).map(|id| {
max_depth=max_depth.max(tree.insert(data,id as u32, dist,0));
if (id>0) && (id%100_000 == 0) {
println!("Inserting ID {}, Max Depth: {}",id,max_depth);
}
}).max();
println!("Max Depth: {}",max_depth);
tree
}
fn from_id(id: u32) -> Self {
let mut ret=Self::default();
ret.ids.insert(id);
return ret;
}
fn insert(&mut self, data: &[String],id: u32, dist: &eddie::str::Levenshtein, depth: usize) -> usize {
if self.is_empty() {
self.ids.insert(id);
return depth;
}
let idx = self.get_id().unwrap() as usize;
let self_key = data.get(idx).unwrap();
let ins_key = data.get(id as usize).unwrap();
let dist_key = dist.distance(self_key,ins_key) as u8;
if dist_key==0 {
self.ids.insert(id);
return depth;
}
if let Some(child) = self.children.get_mut(&dist_key) {
return child.insert(data,id,dist,depth+1);
} else {
self.children.insert(dist_key,Self::from_id(id));
return depth;
}
}
fn get_id(&self) -> Option<u32> {
self.ids.iter().copied().next()
}
fn is_empty(&self) -> bool {
return self.ids.is_empty();
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct BKTree {
base_id: u32,
root: BKTreeNode,
}
impl BKTree {
pub fn new(data: &[String], base_id: u32) -> Self {
let dist = eddie::str::Levenshtein::new();
let root = BKTreeNode::new(data, &dist);
Self {base_id,root}
}
pub fn id(&self) -> u32 {
self.base_id
}
pub fn dump(&self, fh: &mut BufWriter<File>) -> EdLrrResult<()> {
let options = bincode::DefaultOptions::new();
let amt = options.serialized_size(self)?;
println!("Writing {}",amt);
options.serialize_into(fh,self)?;
Ok(())
}
pub fn lookup(&self, name: &str) -> u32 {
todo!();
}
}

0
rust/src/dot_impls.rs Normal file
View File

View File

@ -1,196 +0,0 @@
use crate::common::get_mult;
use crate::common::SystemSerde;
use fnv::FnvHashMap;
use pyo3::prelude::*;
use serde::Deserialize;
use serde_json::Result;
use std::fs::File;
use std::io::Seek;
use std::io::{BufRead, BufReader, BufWriter, SeekFrom};
use std::path::PathBuf;
use std::str;
use std::time::Instant;
#[derive(Debug, Deserialize)]
#[allow(non_snake_case)]
struct Body {
name: String,
subType: String,
#[serde(rename = "type")]
body_type: String,
systemId: i32,
systemId64: i64,
#[serde(rename = "distanceToArrival")]
distance: f32,
}
#[derive(Debug, Deserialize)]
struct Coords {
x: f32,
y: f32,
z: f32,
}
#[derive(Debug, Deserialize)]
struct System {
id: i32,
id64: i64,
name: String,
coords: Coords,
}
#[derive(Debug)]
pub struct PreprocessState {
pub file: String,
pub message: String,
pub total: u64,
pub done: u64,
pub count: usize,
}
fn process(
path: &PathBuf,
func: &mut dyn for<'r> FnMut(&'r str) -> (),
callback: &dyn Fn(&PreprocessState) -> PyResult<PyObject>,
) -> std::io::Result<()> {
let mut buffer = String::new();
let fh = File::open(path)?;
let total_size = fh.metadata()?.len();
let mut t_last = Instant::now();
let mut reader = BufReader::new(fh);
let mut state = PreprocessState {
file: path.to_str().unwrap().to_owned(),
total: total_size,
done: 0,
count: 0,
message: format!("Processing {} ...", path.to_str().unwrap()),
};
println!("Loading {} ...", path.to_str().unwrap());
while let Ok(n) = reader.read_line(&mut buffer) {
if n == 0 {
break;
}
buffer = buffer.trim_end().trim_end_matches(|c| c == ',').to_string();
if !buffer.is_empty() {
func(&buffer);
}
let pos = reader.seek(SeekFrom::Current(0)).unwrap();
state.done = pos;
state.count += 1;
if t_last.elapsed().as_millis() > 100 {
callback(&state)?;
t_last = Instant::now();
}
buffer.clear();
}
Ok(())
}
fn process_systems(
path: &PathBuf,
callback: &dyn Fn(&PreprocessState) -> PyResult<PyObject>,
) -> FnvHashMap<i32, System> {
let mut ret = FnvHashMap::default();
process(
path,
&mut |line| {
let sys_res: Result<System> = serde_json::from_str(&line);
if let Ok(sys) = sys_res {
ret.insert(sys.id, sys);
} else {
eprintln!("\nError parsing: {}\n\t{:?}\n", line, sys_res.unwrap_err());
}
},
callback,
)
.unwrap();
ret
}
pub fn build_index(path: &PathBuf) -> std::io::Result<()> {
let mut wtr = BufWriter::new(File::create(path.with_extension("idx"))?);
let mut idx: Vec<u64> = Vec::new();
let mut records = (csv::Reader::from_path(path)?).into_deserialize::<SystemSerde>();
loop {
idx.push(records.reader().position().byte());
if records.next().is_none() {
break;
}
}
bincode::serialize_into(&mut wtr, &idx).unwrap();
Ok(())
}
fn process_bodies(
path: &PathBuf,
out_path: &PathBuf,
systems: &mut FnvHashMap<i32, System>,
callback: &dyn Fn(&PreprocessState) -> PyResult<PyObject>,
) -> std::io::Result<()> {
println!(
"Processing {} into {} ...",
path.to_str().unwrap(),
out_path.to_str().unwrap(),
);
let mut n: u32 = 0;
let mut wtr = csv::Writer::from_path(out_path)?;
process(
path,
&mut |line| {
if !line.contains("Star") {
return;
}
let body_res: Result<Body> = serde_json::from_str(&line);
if let Ok(body) = body_res {
if !body.body_type.contains("Star") {
return;
}
if let Some(sys) = systems.get(&body.systemId) {
let sub_type = body.subType;
let mult = get_mult(&sub_type);
let sys_name = sys.name.clone();
let rec = SystemSerde {
id: n,
star_type: sub_type,
system: sys_name,
body: body.name,
mult,
distance: body.distance,
x: sys.coords.x,
y: sys.coords.y,
z: sys.coords.z,
};
wtr.serialize(rec).unwrap();
n += 1;
};
} else {
eprintln!("\nError parsing: {}\n\t{:?}\n", line, body_res.unwrap_err());
}
},
callback,
)
.unwrap();
println!("Total Systems: {}", n);
systems.clear();
Ok(())
}
pub fn preprocess_files(
bodies: &PathBuf,
systems: &PathBuf,
out_path: &PathBuf,
callback: &dyn Fn(&PreprocessState) -> PyResult<PyObject>,
) -> std::io::Result<()> {
if !out_path.exists() {
let mut systems = process_systems(systems, &callback);
process_bodies(bodies, out_path, &mut systems, &callback)?;
} else {
println!(
"File '{}' exists, not overwriting it",
out_path.to_str().unwrap()
);
}
println!("Building index...");
println!("Index result: {:?}", build_index(&out_path));
Ok(())
}

View File

@ -1,40 +1,25 @@
use serde::{Deserialize, Serialize};
//! Spansh galaxy.json to csv converter
use crate::common::{get_mult, System};
use eyre::Result;
use flate2::bufread::GzDecoder;
use log::*;
use serde::Deserialize;
use std::fs::File;
use std::io::{BufRead, BufReader, BufWriter};
use std::io::{BufRead, BufReader, BufWriter, Seek};
use std::path::Path;
use std::str;
#[derive(Debug, Clone, Serialize)]
pub struct SystemSerde {
pub id: u32,
pub star_type: String,
pub system: String,
pub body: String,
pub mult: f32,
pub distance: f32,
pub x: f32,
pub y: f32,
pub z: f32,
}
fn get_mult(star_type: &str) -> f32 {
if star_type.contains("White Dwarf") {
return 1.5;
}
if star_type.contains("Neutron") {
return 4.0;
}
1.0
}
#[derive(Debug, Deserialize)]
struct Coords {
#[derive(Debug, Deserialize, Clone)]
struct GalaxyCoords {
x: f32,
y: f32,
z: f32,
}
#[derive(Debug, Deserialize)]
struct Body {
#[derive(Debug, Deserialize, Clone)]
struct GalaxyBody {
name: String,
#[serde(rename = "type")]
body_type: String,
@ -44,70 +29,73 @@ struct Body {
distance: f32,
}
#[derive(Debug, Deserialize)]
struct System {
coords: Coords,
#[derive(Debug, Deserialize, Clone)]
struct GalaxySystem {
coords: GalaxyCoords,
name: String,
bodies: Vec<Body>,
bodies: Vec<GalaxyBody>,
}
pub fn process_galaxy_dump(path: &str) -> std::io::Result<()> {
let fh = File::create("stars.csv")?;
let mut wtr = csv::Writer::from_writer(BufWriter::new(fh));
/// Load compressed galaxy.json from `path` and write `stars.csv` to `out_path`
pub fn process_galaxy_dump(path: &Path, out_path: &Path) -> Result<()> {
let out_path = out_path.with_extension("csv");
let mut wtr = csv::WriterBuilder::new()
.has_headers(false)
.from_writer(BufWriter::new(File::create(out_path)?));
let mut buffer = String::new();
let mut bz2_reader = std::process::Command::new("7z")
.args(&["x", "-so", path])
.stdout(std::process::Stdio::piped())
.spawn()
.unwrap_or_else(|err| {
eprintln!("Failed to run 7z: {}", err);
eprintln!("Falling back to bzip2");
std::process::Command::new("bzip2")
.args(&["-d", "-c", path])
.stdout(std::process::Stdio::piped())
.spawn()
.expect("Failed to execute bzip2!")
});
let mut reader = BufReader::new(
bz2_reader
.stdout
.as_mut()
.expect("Failed to open stdout of child process"),
);
let mut count = 0;
let rdr = BufReader::new(File::open(path)?);
let mut reader = BufReader::new(GzDecoder::new(rdr));
let mut count: usize = 0;
let mut total: usize = 0;
let mut errors: usize = 0;
let mut bodies: usize = 0;
let mut systems = 0;
let max_len = File::metadata(reader.get_ref().get_ref().get_ref())?.len();
while let Ok(n) = reader.read_line(&mut buffer) {
if n == 0 {
break;
}
buffer = buffer
.trim()
.trim_end_matches(|c| c == ',')
.trim()
.trim_end_matches(|c: char| c == ',' || c.is_whitespace())
.to_string();
if !buffer.contains("Star") {
continue;
};
if let Ok(sys) = serde_json::from_str::<System>(&buffer) {
for b in &sys.bodies {
if b.body_type == "Star" {
let s = SystemSerde {
id: count,
star_type: b.sub_type.clone(),
distance: b.distance,
mult: get_mult(&b.sub_type),
body: b.name.clone(),
system: sys.name.clone(),
x: sys.coords.x,
y: sys.coords.y,
z: sys.coords.z,
};
wtr.serialize(s)?;
count += 1;
total += 1;
if let Ok(sys) = serde_json::from_str::<GalaxySystem>(&buffer) {
let mut sys_rec = System {
id: systems,
mult: 1.0,
name: sys.name,
num_bodies: 0,
pos: [sys.coords.x, sys.coords.y, sys.coords.z],
has_scoopable: false,
};
for b in sys.bodies.iter().filter(|b| b.body_type == "Star").cloned() {
sys_rec.mult = sys_rec.mult.max(get_mult(&b.sub_type));
sys_rec.num_bodies += 1;
for c in "KGBFOAM".chars() {
if b.sub_type.starts_with(c) {
sys_rec.has_scoopable |= true;
break;
}
}
}
bodies += sys_rec.num_bodies as usize;
systems += 1;
count += 1;
wtr.serialize(sys_rec)?;
if count % 100_000 == 0 {
let cur_pos = reader.get_ref().get_ref().get_ref().stream_position()?;
let prc: f64 = ((cur_pos as f64) / (max_len as f64)) * 100.0;
info!("[{:.2} %] {} systems written", prc, count);
}
} else {
errors += 1;
}
buffer.clear();
}
println!("Total: {}", count);
info!("Total: {}", total);
info!("Bodies: {}", bodies);
info!("Systems: {}", systems);
info!("Processed: {}", count);
info!("Errors: {}", errors);
Ok(())
}

View File

@ -1,6 +1,8 @@
//! Elite: Dangerous Journal Loadout even parser
use crate::common::get_fsd_info;
use crate::ship::Ship;
use eyre::Result;
use regex::Regex;
use serde::Deserialize;
use std::collections::HashMap;
@ -11,23 +13,23 @@ pub struct Event {
pub event: EventData,
}
#[serde(tag = "event")]
#[derive(Clone, Debug, PartialEq, Deserialize)]
#[serde(tag = "event")]
pub enum EventData {
Loadout(Loadout),
#[serde(other)]
Unknown,
}
#[serde(rename_all = "PascalCase")]
#[derive(Clone, Debug, PartialEq, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct Modifier {
label: String,
value: f32,
}
#[serde(rename_all = "PascalCase")]
#[derive(Clone, Debug, PartialEq, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct Engineering {
modifiers: Vec<Modifier>,
}
@ -72,55 +74,46 @@ impl Engineering {
impl Loadout {
fn get_booster(&self) -> Option<usize> {
self.modules
.iter()
.cloned()
.filter_map(|m| {
let Module { item, .. } = m;
if item.starts_with("int_guardianfsdbooster") {
return item
.chars()
.last()
.unwrap()
.to_digit(10)
.map(|v| v as usize);
}
return None;
})
.next()
self.modules.iter().cloned().find_map(|m| {
let Module { item, .. } = m;
if item.starts_with("int_guardianfsdbooster") {
return item
.chars()
.last()
.unwrap()
.to_digit(10)
.map(|v| v as usize);
}
return None;
})
}
fn get_fsd(&self) -> Option<(String, Option<Engineering>)> {
self.modules
.iter()
.cloned()
.filter_map(|m| {
let Module {
slot,
engineering,
item,
} = m;
if slot == "FrameShiftDrive" {
return Some((item, engineering));
}
return None;
})
.next()
self.modules.iter().cloned().find_map(|m| {
let Module {
slot,
engineering,
item,
} = m;
if slot == "FrameShiftDrive" {
return Some((item, engineering));
}
return None;
})
}
pub fn try_into_ship(self) -> Result<Ship, String> {
pub fn try_into_ship(self) -> Result<(String, Ship), String> {
let fsd = self.get_fsd().ok_or("No FSD found!")?;
let booster = self.get_booster().unwrap_or(0);
let fsd_type = Regex::new(r"^int_hyperdrive_size(\d+)_class(\d+)$")
.unwrap()
.captures(&fsd.0);
let fsd_type: (usize, usize) = fsd_type
.map(|m| {
.and_then(|m| {
let s = m.get(1)?.as_str().to_owned().parse().ok()?;
let c = m.get(2)?.as_str().to_owned().parse().ok()?;
return Some((c, s));
})
.flatten()
.ok_or(format!("Invalid FSD found: {}", &fsd.0))?;
let eng = fsd
.1
@ -141,18 +134,28 @@ impl Loadout {
let opt_mass = fsd_info
.get("FSDOptimalMass")
.ok_or(format!("Unknwon FSDOptimalMass for FSD: {}", &fsd.0))?;
return Ship::new(
self.ship_name,
let key = format!(
"[{}] {} ({})",
if !self.ship_name.is_empty() {
self.ship_name
} else {
"<NO NAME>".to_owned()
},
self.ship_ident,
self.ship,
self.unladen_mass,
self.fuel_capacity.main,
self.fuel_capacity.main,
fsd_type,
*max_fuel,
*opt_mass,
booster,
self.ship
);
return Ok((
key,
Ship::new(
self.unladen_mass,
self.fuel_capacity.main,
self.fuel_capacity.main,
fsd_type,
*max_fuel,
*opt_mass,
booster,
)?,
));
}
}

View File

@ -1,144 +1,379 @@
// #![deny(warnings)]
#![allow(dead_code, clippy::needless_return, clippy::too_many_arguments)]
//! # Elite: Danerous Long Range Router
pub mod common;
pub mod edsm;
pub mod galaxy;
pub mod journal;
pub mod mmap_csv;
#[cfg(feature = "profiling")]
pub mod profiling;
pub mod route;
pub mod search_algos;
pub mod ship;
use bincode::Options;
use csv::{Position, StringRecord};
use eddie::Levenshtein;
// =========================
use stats_alloc::{Region, StatsAlloc, INSTRUMENTED_SYSTEM};
use std::alloc::System as SystemAlloc;
use std::cell::RefMut;
use std::collections::BTreeMap;
use std::io::{BufWriter, Write};
use std::path::Path;
use std::time::Instant;
#[cfg(not(feature = "profiling"))]
#[global_allocator]
static GLOBAL: &StatsAlloc<SystemAlloc> = &INSTRUMENTED_SYSTEM;
// =========================
#[cfg(not(feature = "profiling"))]
mod profiling {
pub fn init() {}
}
extern crate derivative;
use crate::common::{find_matches, SysEntry};
use crate::common::{find_matches, grid_stats, EdLrrError, SysEntry, System};
#[cfg(feature = "profiling")]
use crate::profiling::*;
use crate::route::{Router, SearchState};
use crate::ship::Ship;
use eyre::Result;
#[cfg(not(feature = "profiling"))]
use log::*;
use pyo3::exceptions::*;
use pyo3::prelude::*;
use pyo3::types::{PyDict, PyList, PyTuple};
use pyo3::PyObjectProtocol;
use std::path::PathBuf;
use pyo3::types::{IntoPyDict, PyDict, PyTuple};
use pyo3::{create_exception, PyObjectProtocol};
use route::{LineCache, PyModeConfig};
use std::{
cell::RefCell, collections::HashMap, convert::TryInto, fs::File, io::BufReader, path::PathBuf,
};
#[cfg(feature = "profiling")]
#[global_allocator]
static GLOBAL: ProfiledAllocator<std::alloc::System> =
ProfiledAllocator::new(std::alloc::System, 1024);
create_exception!(_ed_lrr, RoutingError, PyException);
create_exception!(_ed_lrr, ProcessingError, PyException);
create_exception!(_ed_lrr, ResolveError, PyException);
#[derive(Debug)]
enum RangeOrShip {
Range(f32),
Ship(Ship),
}
impl FromPyObject<'_> for RangeOrShip {
fn extract(ob: &PyAny) -> PyResult<Self> {
if let Ok(n) = ob.extract() {
return Ok(Self::Range(n));
}
let s: PyShip = ob.extract()?;
return Ok(Self::Ship(s.ship));
}
}
#[pyclass(dict)]
#[derive(Debug)]
#[text_signature = "(callback, /)"]
#[pyo3(text_signature = "(callback, /)")]
struct PyRouter {
router: Router,
primary_only: bool,
stars_path: String,
stars_path: Option<String>,
}
impl PyRouter {
fn check_stars(&self) -> PyResult<PathBuf> {
self.stars_path
.as_ref()
.ok_or_else(|| PyErr::from(EdLrrError::RuntimeError("no stars.csv loaded".to_owned())))
.map(PathBuf::from)
}
}
#[pymethods]
impl PyRouter {
#[new]
#[args(callback = "None")]
fn new(callback: Option<PyObject>, py: Python<'static>) -> PyResult<Self> {
Ok(PyRouter {
router: Router::new(Box::new(
move |state: &SearchState| {
match callback.as_ref() {
Some(cb) => cb.call(py, (state.clone(),), None),
None => Ok(py.None()),
}
fn new(callback: Option<PyObject>) -> Self {
let mut router = Router::new();
if callback.is_some() {
router.set_callback(Box::new(move |state: &SearchState| {
let gil_guard = Python::acquire_gil();
let py = gil_guard.python();
match callback.as_ref() {
Some(cb) => cb.call(py, (state.clone(),), None),
None => Ok(py.None()),
}
)),
primary_only: false,
stars_path: String::from(""),
})
}))
}
PyRouter {
router,
stars_path: None,
}
}
#[text_signature = "(ship, /)"]
fn set_ship(&mut self, py: Python, ship: &PyShip) -> PyResult<PyObject> {
self.router.set_ship(ship.ship.clone());
#[args(primary_only = "false", immediate = "false")]
#[pyo3(text_signature = "(path, primary_only, /)")]
fn load(&mut self, path: String, py: Python, immediate: bool) -> PyResult<PyObject> {
self.stars_path = Some(path);
if immediate {
let stars_path = self.check_stars()?;
let route_res = self.router.load(&stars_path);
if let Err(err_msg) = route_res {
return Err(PyErr::new::<PyValueError, _>(err_msg));
};
}
Ok(py.None())
}
#[args(primary_only = "false")]
#[text_signature = "(path, primary_only, /)"]
fn load(
&mut self,
path: String,
primary_only: bool,
py: Python,
) -> PyResult<PyObject> {
self.stars_path = path;
self.primary_only = primary_only;
#[pyo3(text_signature = "(/)")]
fn unload(&mut self, py: Python) -> PyObject {
self.router.unload();
py.None()
}
fn plot(&mut self, py: Python) -> PyResult<PyObject> {
let stars_path = self.check_stars()?;
let route_res = self.router.load(&stars_path);
if let Err(err_msg) = route_res {
return Err(PyErr::new::<PyValueError, _>(err_msg));
};
let mut max_v = [0f32, 0f32, 0f32];
let mut min_v = [0f32, 0f32, 0f32];
for node in self.router.get_tree().iter() {
for i in 0..3 {
if node.pos[i] > max_v[i] {
max_v[i] = node.pos[i];
}
if node.pos[i] < min_v[i] {
min_v[i] = node.pos[i];
}
}
}
let plot_bbox: ((f32, f32), (f32, f32)) = ((min_v[0], max_v[0]), (min_v[2], max_v[2]));
Ok(plot_bbox.to_object(py))
}
fn run_bfs(&mut self, range: f32, py: Python) -> PyResult<PyObject> {
let stars_path = self.check_stars()?;
let route_res = self.router.load(&stars_path);
if let Err(err_msg) = route_res {
return Err(PyErr::new::<PyValueError, _>(err_msg));
};
self.router
.precomp_bfs(range)
.map_err(PyErr::new::<RoutingError, _>)
.map(|_| py.None())
}
fn precompute_graph(&mut self, range: f32, py: Python) -> PyResult<PyObject> {
let stars_path = self.check_stars()?;
let route_res = self.router.load(&stars_path);
if let Err(err_msg) = route_res {
return Err(PyErr::new::<PyValueError, _>(err_msg));
};
self.router
.precompute_graph(range)
.map_err(PyErr::new::<RoutingError, _>)
.map(|_| py.None())
}
fn nb_perf_test(&mut self, range: f32, py: Python) -> PyResult<PyObject> {
let stars_path = self.check_stars()?;
let route_res = self.router.load(&stars_path);
if let Err(err_msg) = route_res {
return Err(PyErr::new::<PyValueError, _>(err_msg));
};
let mut nbmap = BTreeMap::new();
let tree = self.router.get_tree();
let total_nodes = tree.size();
for (n, node) in tree.iter().enumerate() {
let nbs = self
.router
.neighbours(node, range)
.map(|nb| nb.id)
.collect::<Vec<_>>();
nbmap.insert(node.id, nbs);
if n % 100_000 == 0 {
println!("{}/{}", n, total_nodes);
}
}
println!("{}", nbmap.len());
Ok(py.None())
}
#[args(greedyness = "0.5", num_workers = "0", beam_width = "0")]
#[text_signature = "(hops, range, greedyness, beam_width, num_workers, /)"]
fn precompute_neighbors(&mut self, range: f32, py: Python) -> PyResult<PyObject> {
let stars_path = self.check_stars()?;
let route_res = self.router.load(&stars_path);
if let Err(err_msg) = route_res {
return Err(PyErr::new::<PyValueError, _>(err_msg));
};
self.router
.precompute_all(range)
.map_err(PyErr::new::<RoutingError, _>)
.map(|_| py.None())
}
#[args(
greedyness = "0.5",
max_dist = "0.0",
num_workers = "0",
beam_width = "BeamWidth::Absolute(0)"
)]
#[pyo3(text_signature = "(hops, range, mode, num_workers, /)")]
fn route(
&mut self,
hops: &PyList,
range: Option<f32>,
greedyness: f32,
beam_width: usize,
hops: Vec<SysEntry>,
range: RangeOrShip,
mode: Option<PyModeConfig>,
num_workers: usize,
py: Python,
) -> PyResult<PyObject> {
let route_res = self
.router
.load(&PathBuf::from(self.stars_path.clone()), self.primary_only);
) -> PyResult<Vec<common::System>> {
let stars_path = self.check_stars()?;
let route_res = self.router.load(&stars_path);
if let Err(err_msg) = route_res {
return Err(PyErr::new::<ValueError, _>(err_msg));
return Err(PyErr::new::<PyValueError, _>(err_msg));
};
let mut sys_entries: Vec<SysEntry> = Vec::new();
for hop in hops {
if let Ok(id) = hop.extract() {
sys_entries.push(SysEntry::ID(id));
} else {
sys_entries.push(SysEntry::parse(hop.extract()?));
}
}
println!("Resolving systems...");
let ids: Vec<u32> = match resolve(&sys_entries, &self.router.path) {
Ok(ids) => ids,
info!("Resolving systems...");
let ids: Vec<u32> = match resolve(&hops, &self.router.path, true) {
Ok(sytems) => sytems.into_iter().map(|id| id.into_id()).collect(),
Err(err_msg) => {
return Err(PyErr::new::<ValueError, _>(err_msg));
return Err(EdLrrError::ResolveError(err_msg).into());
}
};
match self
.router
.compute_route(&ids, range, greedyness, beam_width, num_workers)
{
Ok(route) => {
let py_route: Vec<_> = route.iter().map(|hop| hop.to_object(py)).collect();
Ok(py_route.to_object(py))
let mut is_default = false;
let mut is_ship = false;
info!("{:?}", mode);
let mut mode = match mode {
Some(mode) => mode,
None => {
let mode = PyModeConfig::default();
is_default = true;
mode
}
};
if mode.mode.is_empty() {
if mode.ship.is_none() {
mode.mode = "bfs".to_string();
} else {
mode.mode = "ship".to_string();
if mode.ship_mode == *"" {
mode.ship_mode = "jumps".to_string();
}
}
Err(err_msg) => Err(PyErr::new::<RuntimeError, _>(err_msg)),
}
let range = match range {
RangeOrShip::Range(r) => Some(r),
RangeOrShip::Ship(ship) => {
mode.mode = "ship".into();
mode.ship = Some(ship);
is_ship = true;
None
}
};
info!("{:?}", mode);
let mode = mode.try_into()?;
if is_default && !is_ship {
warn!("no mode specified, defaulting to {}", mode);
}
#[cfg(not(feature = "profiling"))]
let reg = Region::new(GLOBAL);
let res = match self.router.compute_route(&ids, range, mode, num_workers) {
Ok(route) => Ok(route),
Err(err_msg) => Err(PyErr::new::<RoutingError, _>(err_msg)),
};
#[cfg(not(feature = "profiling"))]
println!("{:?}", reg.change());
return res;
}
fn perf_test(&self, callback: PyObject, py: Python) -> PyResult<PyObject> {
use common::TreeNode;
let node = TreeNode {
pos: [-65.21875, 7.75, -111.03125],
flags: 1,
id: 0,
};
let goal = TreeNode {
pos: [-9530.5, -910.28125, 19808.125],
flags: 1,
id: 1,
};
let kwargs = vec![("goal", goal), ("node", node)].into_py_dict(py);
let mut n: usize = 0;
let mut d: f64 = 0.0;
let num_loops = 10_000_000;
loop {
let pool = unsafe { Python::new_pool(py) };
let t_start = std::time::Instant::now();
for _ in 0..num_loops {
let val: f64 = callback.call(py, (), Some(kwargs))?.extract(py)?;
}
d += t_start.elapsed().as_secs_f64();
drop(pool);
n += num_loops;
let dt = std::time::Duration::from_secs_f64(d / (n as f64));
println!("{}: {:?}", n, dt);
}
Ok(py.None())
}
#[args(grid_size = "1.0")]
#[pyo3(text_signature = "(grid_size)")]
fn get_grid(&self, grid_size: f32, py: Python) -> PyResult<PyObject> {
let stars_path = self.check_stars()?;
grid_stats(&stars_path, grid_size)
.map(|ret| ret.to_object(py))
.map_err(PyErr::new::<PyRuntimeError, _>)
}
#[args(hops = "*")]
#[text_signature = "(sys_1, sys_2, ..., /)"]
fn resolve_systems(&self, hops: &PyTuple, py: Python) -> PyResult<PyObject> {
let mut sys_entries: Vec<SysEntry> = Vec::new();
for hop in hops {
if let Ok(id) = hop.extract() {
sys_entries.push(SysEntry::ID(id));
} else {
sys_entries.push(SysEntry::parse(hop.extract()?));
}
}
println!("Resolving systems...");
let stars_path = PathBuf::from(self.stars_path.clone());
let ids: Vec<u32> = match resolve(&sys_entries, &stars_path) {
Ok(ids) => ids,
#[pyo3(text_signature = "(sys_1, sys_2, ..., /)")]
fn resolve(&self, hops: Vec<SysEntry>, py: Python) -> PyResult<PyObject> {
info!("Resolving systems...");
let stars_path = self.check_stars()?;
let systems: Vec<System> = match resolve(&hops, &stars_path, false) {
Ok(sytems) => sytems.into_iter().map(|sys| sys.into_system()).collect(),
Err(err_msg) => {
return Err(PyErr::new::<ValueError, _>(err_msg));
return Err(EdLrrError::ResolveError(err_msg).into());
}
};
let ret: Vec<(_, u32)> = hops.into_iter().zip(ids.into_iter()).collect();
let ret: Vec<(_, System)> = hops
.into_iter()
.zip(systems.iter())
.map(|(id, sys)| (id, sys.clone()))
.collect();
Ok(PyDict::from_sequence(py, ret.to_object(py))?.to_object(py))
}
#[staticmethod]
fn preprocess_edsm() -> PyResult<()> {
todo!("Implement EDSM Preprocessor")
}
#[staticmethod]
fn preprocess_galaxy() -> PyResult<()> {
todo!("Implement galaxy.json Preprocessor")
fn str_tree_test(&self) -> common::EdLrrResult<()> {
use common::BKTree;
const CHUNK_SIZE: usize = 4_000_000;
let path = self.check_stars()?;
let reader: csv::Reader<File> = csv::ReaderBuilder::new()
.has_headers(false)
.from_path(path)
.map_err(EdLrrError::from)?;
let mut data: Vec<String> = Vec::with_capacity(CHUNK_SIZE);
let t_start = Instant::now();
let mut base_id=0;
let mut wr = BufWriter::new(File::create("test.bktree")?);
for sys in reader.into_deserialize::<System>() {
let sys = sys?;
data.push(sys.name);
if data.len()>CHUNK_SIZE {
let tree = BKTree::new(&data, base_id);
tree.dump(&mut wr)?;
base_id=sys.id;
}
}
if !data.is_empty() {
let tree = BKTree::new(&data, base_id);
tree.dump(&mut wr)?;
}
wr.flush()?;
println!("Took: {:?}", t_start.elapsed());
Ok(())
}
}
@ -153,52 +388,91 @@ impl PyObjectProtocol for PyRouter {
}
}
fn resolve(entries: &[SysEntry], path: &PathBuf) -> Result<Vec<u32>, String> {
enum ResolveResult {
System(System),
ID(u32),
}
impl ResolveResult {
fn into_id(self) -> u32 {
match self {
Self::System(sys) => sys.id,
Self::ID(id) => id,
}
}
fn into_system(self) -> System {
if let Self::System(sys) = self {
return sys;
}
panic!("Tried to unwrap ID into System");
}
}
fn resolve(entries: &[SysEntry], path: &Path, id_only: bool) -> Result<Vec<ResolveResult>, String> {
let mut names: Vec<String> = Vec::new();
let mut ids: Vec<u32> = Vec::new();
let mut ret: Vec<u32> = Vec::new();
let mut needs_rtree = false;
for ent in entries {
match ent {
SysEntry::Name(name) => names.push(name.to_owned()),
SysEntry::ID(id) => ids.push(*id),
SysEntry::Pos(_) => {
needs_rtree |= true;
}
_ => (),
}
}
if !path.exists() {
return Err(format!(
"Source file \"{:?}\" does not exist!",
path.display()
));
return Err(format!("Source file {:?} does not exist!", path.display()));
}
let name_ids = find_matches(path, names, false)?;
let name_ids = if !names.is_empty() {
mmap_csv::mmap_csv(path, names)?
} else {
HashMap::new()
};
let tmp_r = needs_rtree
.then(|| {
let mut r = Router::new();
r.load(path).map(|_| r)
})
.transpose()?;
for ent in entries {
match ent {
SysEntry::Name(name) => {
let ent_res = name_ids
.get(&name.to_owned())
.get(name)
.ok_or(format!("System {} not found", name))?;
let sys = ent_res
.1
.as_ref()
.ok_or(format!("System {} not found", name))?;
if ent_res.0 < 0.75 {
println!(
"WARNING: {} match to {} with low confidence ({:.2}%)",
name,
sys.system,
ent_res.0 * 100.0
);
}
ret.push(sys.id);
ret.push(*sys);
}
SysEntry::ID(id) => ret.push(*id),
SysEntry::Pos((x, y, z)) => ret.push(
tmp_r
.as_ref()
.unwrap()
.closest(&[*x, *y, *z])
.ok_or("No systems loaded!")?
.id,
),
}
}
Ok(ret)
if id_only {
return Ok(ret.iter().map(|id| ResolveResult::ID(*id)).collect());
} else {
let mut lc = route::LineCache::create(path)?;
let mut systems = vec![];
for id in ret {
let sys = ResolveResult::System(lc.get(id)?.unwrap());
systems.push(sys)
}
return Ok(systems);
}
}
#[pyclass(dict)]
#[derive(Debug)]
#[derive(Debug, Clone)]
struct PyShip {
ship: Ship,
}
@ -219,8 +493,8 @@ impl PyShip {
#[staticmethod]
fn from_loadout(py: Python, loadout: &str) -> PyResult<PyObject> {
match Ship::new_from_json(loadout) {
Ok(ship) => Ok((PyShip { ship }).into_py(py)),
Err(err_msg) => Err(PyErr::new::<ValueError, _>(err_msg)),
Ok(ship) => Ok((PyShip { ship: ship.1 }).into_py(py)),
Err(err_msg) => Err(PyErr::new::<PyValueError, _>(err_msg)),
}
}
#[staticmethod]
@ -228,15 +502,15 @@ impl PyShip {
let mut ship = match Ship::new_from_journal() {
Ok(ship) => ship,
Err(err_msg) => {
return Err(PyErr::new::<ValueError, _>(err_msg));
return Err(PyErr::new::<PyValueError, _>(err_msg));
}
};
let ships: Vec<(PyObject, PyObject)> = ship
.drain()
.map(|(k, v)| {
let k_py = k.to_object(py);
let v_py = (PyShip { ship: v }).into_py(py);
(k_py, v_py)
.map(|(ship_name, ship)| {
let ship_name_py = ship_name.to_object(py);
let ship_py = (PyShip { ship }).into_py(py);
(ship_name_py, ship_py)
})
.collect();
Ok(PyDict::from_sequence(py, ships.to_object(py))?.to_object(py))
@ -246,239 +520,141 @@ impl PyShip {
self.ship.to_object(py)
}
#[text_signature = "(dist, /)"]
fn fuel_cost(&self, _py: Python, dist: f32) -> PyResult<f32> {
Ok(self.ship.fuel_cost(dist))
#[pyo3(text_signature = "(dist, /)")]
fn fuel_cost(&self, _py: Python, dist: f32) -> f32 {
self.ship.fuel_cost(dist)
}
#[text_signature = "(/)"]
fn range(&self, _py: Python) -> PyResult<f32> {
Ok(self.ship.range())
#[getter]
fn range(&self, _py: Python) -> f32 {
self.ship.range()
}
#[text_signature = "(/)"]
fn max_range(&self, _py: Python) -> PyResult<f32> {
Ok(self.ship.max_range())
#[getter]
fn max_range(&self, _py: Python) -> f32 {
self.ship.max_range()
}
#[text_signature = "(dist, /)"]
fn make_jump(&mut self, dist: f32, _py: Python) -> PyResult<Option<f32>> {
Ok(self.ship.make_jump(dist))
#[pyo3(text_signature = "(dist, /)")]
fn make_jump(&mut self, dist: f32, _py: Python) -> Option<f32> {
self.ship.make_jump(dist)
}
#[text_signature = "(dist, /)"]
fn can_jump(&self, dist: f32, _py: Python) -> PyResult<bool> {
Ok(self.ship.can_jump(dist))
#[pyo3(text_signature = "(dist, /)")]
fn can_jump(&self, dist: f32, _py: Python) -> bool {
self.ship.can_jump(dist)
}
#[args(fuel_amount = "None")]
#[text_signature = "(fuel_amount, /)"]
fn refuel(&mut self, fuel_amount: Option<f32>, _py: Python) -> PyResult<()> {
#[pyo3(text_signature = "(fuel_amount, /)")]
fn refuel(&mut self, fuel_amount: Option<f32>, _py: Python) {
if let Some(fuel) = fuel_amount {
self.ship.fuel_mass = (self.ship.fuel_mass + fuel).min(self.ship.fuel_capacity)
} else {
self.ship.fuel_mass = self.ship.fuel_capacity;
}
Ok(())
}
#[text_signature = "(factor, /)"]
fn boost(&mut self, factor: f32, _py: Python) -> PyResult<()> {
#[pyo3(text_signature = "(factor, /)")]
fn boost(&mut self, factor: f32, _py: Python) {
self.ship.boost(factor);
Ok(())
}
}
impl PyShip {
fn get_ship(&self) -> Ship {
self.ship.clone()
}
}
#[pyfunction]
fn preprocess_edsm(
_bodies_path: &str,
_systems_path: &str,
_out_path: &str,
_py: Python,
) -> PyResult<()> {
Err(pyo3::exceptions::PyNotImplementedError::new_err(
"please use Spansh's Galaxy dump and preprocess_galaxy()",
))
}
fn to_py_value(value: eval::Value, py: Python) -> PyResult<PyObject> {
type Value = eval::Value;
match value {
Value::String(s) => Ok(s.to_object(py)),
Value::Number(n) => {
if let Some(n) = n.as_u64() {
return Ok(n.to_object(py));
}
if let Some(n) = n.as_i64() {
return Ok(n.to_object(py));
}
return Ok(n.as_f64().unwrap().to_object(py));
}
Value::Bool(b) => Ok(b.to_object(py)),
Value::Array(mut t) => {
let mut res: Vec<PyObject> = vec![];
for v in t.drain(..) {
res.push(to_py_value(v, py)?);
}
Ok(PyTuple::new(py, &res).to_object(py))
}
Value::Object(o) => {
let res = PyDict::new(py);
for (k, v) in o.iter() {
res.set_item(k, to_py_value(v.clone(), py)?)?;
}
Ok(res.to_object(py))
}
Value::Null => Ok(py.None()),
}
}
fn to_py(res: Result<eval::Value, eval::Error>, py: Python) -> PyResult<PyObject> {
res.map_err(|e| PyErr::from(EdLrrError::EvalError(e)))
.and_then(|r| to_py_value(r, py))
}
#[pyfunction]
#[pyo3(text_signature = "(expr)")]
fn expr_test(expr: &str, py: Python) -> PyResult<PyObject> {
use eval::{to_value, Expr, Value};
let mut res = Expr::new(expr)
.compile()
.map_err(|e| PyErr::from(EdLrrError::EvalError(e)))?;
let mut hm: HashMap<&str, Value> = HashMap::new();
hm.insert("foo", to_value(42));
hm.insert("bar", to_value((-1, -2, -3)));
res = res.value("x", vec!["Hello", "world", "!"]);
res = res.value("y", 42);
res = res.value("p", (2.17, 5.14, 1.62));
res = res.value("hw", "Hello World!");
res = res.value("hm", hm);
to_py(res.exec(), py)
}
#[pyfunction]
#[pyo3(text_signature = "(path, out_path, /)")]
fn preprocess_galaxy(path: &str, out_path: &str) -> PyResult<()> {
use common::build_index;
use galaxy::process_galaxy_dump;
let path = PathBuf::from(path);
let out_path = PathBuf::from(out_path);
process_galaxy_dump(&path, &out_path).unwrap();
build_index(&out_path)?;
Ok(())
}
#[pymodule]
pub fn _ed_lrr(_py: Python, m: &PyModule) -> PyResult<()> {
better_panic::install();
pyo3_log::init();
profiling::init();
m.add_class::<PyRouter>()?;
m.add_class::<PyShip>()?;
/*
#[pyfn(m, "get_ships_from_journal")]
fn get_ships_from_journal(py: Python) -> PyResult<PyObject> {
let ship = match Ship::new_from_journal() {
Ok(ship) => ship,
Err(err_msg) => {
return Err(PyErr::new::<ValueError, _>(err_msg));
}
};
let ships: Vec<(_,_)> = ship.iter().map(|(k,v)| (k.to_object(py),v.to_object(py))).collect();
Ok(PyDict::from_sequence(py, ships.to_object(py))?.to_object(py))
}
#[pyfn(m, "get_ships_from_loadout")]
fn get_ship_from_loadout(py: Python, loadout: &str) -> PyResult<PyObject> {
let ship = match Ship::new_from_json(loadout) {
Ok(ship) => ship,
Err(err_msg) => {
return Err(PyErr::new::<ValueError, _>(err_msg));
}
};
Ok(ship.to_object(py))
}
*/
m.add_wrapped(pyo3::wrap_pyfunction!(preprocess_galaxy))?;
m.add_wrapped(pyo3::wrap_pyfunction!(preprocess_edsm))?;
m.add_wrapped(pyo3::wrap_pyfunction!(expr_test))?;
Ok(())
}
/*
/// Preprocess bodies.json and systemsWithCoordinates.json into stars.csv
#[pyfn(m, "preprocess")]
#[text_signature = "(infile_systems, infile_bodies, outfile, callback, /)"]
fn ed_lrr_preprocess(
py: Python<'static>,
infile_systems: String,
infile_bodies: String,
outfile: String,
callback: PyObject,
) -> PyResult<PyObject> {
use preprocess::*;
let state = PyDict::new(py);
let state_dict = PyDict::new(py);
callback.call(py, (state_dict,), None).unwrap();
let callback_wrapped = move |state: &PreprocessState| {
// println!("SEND: {:?}",state);
state_dict.set_item("file", state.file.clone())?;
state_dict.set_item("total", state.total)?;
state_dict.set_item("count", state.count)?;
state_dict.set_item("done", state.done)?;
state_dict.set_item("message", state.message.clone())?;
callback.call(py, (state_dict,), None)
};
preprocess_files(
&PathBuf::from(infile_bodies),
&PathBuf::from(infile_systems),
&PathBuf::from(outfile),
&callback_wrapped,
)
.unwrap();
Ok(state.to_object(py))
}
/// Find system by name
#[pyfn(m, "find_sys")]
#[text_signature = "(sys_names, sys_list_path, /)"]
fn find_sys(py: Python, sys_names: Vec<String>, sys_list: String) -> PyResult<PyObject> {
let path = PathBuf::from(sys_list);
match find_matches(&path, sys_names, false) {
Ok(vals) => {
let ret = PyDict::new(py);
for (key, (diff, sys)) in vals {
let ret_dict = PyDict::new(py);
if let Some(val) = sys {
let pos = PyList::new(py, val.pos.iter());
ret_dict.set_item("star_type", val.star_type.clone())?;
ret_dict.set_item("system", val.system.clone())?;
ret_dict.set_item("body", val.body.clone())?;
ret_dict.set_item("distance", val.distance)?;
ret_dict.set_item("pos", pos)?;
ret_dict.set_item("id", val.id)?;
ret.set_item(key, (diff, ret_dict).to_object(py))?;
}
}
Ok(ret.to_object(py))
}
Err(e) => Err(PyErr::new::<ValueError, _>(e)),
}
}
/// Compute a Route using the suplied parameters
#[pyfn(m, "route")]
#[text_signature = "(hops, range, mode, primary, permute, keep_first, keep_last, greedyness, precomp, path, num_workers, callback, /)"]
#[allow(clippy::too_many_arguments)]
fn py_route(
py: Python<'static>,
hops: Vec<&str>,
range: f32,
mode: String,
primary: bool,
permute: bool,
keep_first: bool,
keep_last: bool,
greedyness: Option<f32>,
precomp: Option<String>,
path: String,
num_workers: Option<usize>,
callback: PyObject,
) -> PyResult<PyObject> {
use route::*;
let num_workers = num_workers.unwrap_or(1);
let mode = match Mode::parse(&mode) {
Ok(val) => val,
Err(e) => {
return Err(PyErr::new::<ValueError, _>(e));
}
};
let state_dict = PyDict::new(py);
{
let cb_res = callback.call(py, (state_dict,), None);
if cb_res.is_err() {
println!("Error: {:?}", cb_res);
}
}
let callback_wrapped = move |state: &SearchState| {
state_dict.set_item("mode", state.mode.clone())?;
state_dict.set_item("system", state.system.clone())?;
state_dict.set_item("body", state.body.clone())?;
state_dict.set_item("depth", state.depth)?;
state_dict.set_item("queue_size", state.queue_size)?;
state_dict.set_item("d_rem", state.d_rem)?;
state_dict.set_item("d_total", state.d_total)?;
state_dict.set_item("prc_done", state.prc_done)?;
state_dict.set_item("n_seen", state.n_seen)?;
state_dict.set_item("prc_seen", state.prc_seen)?;
state_dict.set_item("from", state.from.clone())?;
state_dict.set_item("to", state.to.clone())?;
let cb_res = callback.call(py, (state_dict,), None);
if cb_res.is_err() {
println!("Error: {:?}", cb_res);
}
cb_res
};
let hops: Vec<SysEntry> = (hops.iter().map(|v| SysEntry::from_str(&v)).collect::<Result<Vec<SysEntry>,_>>())?;
println!("Resolving systems...");
let hops: Vec<u32> = match resolve(&hops, &PathBuf::from(&path)) {
Ok(ids) => ids,
Err(err_msg) => {
return Err(PyErr::new::<ValueError, _>(err_msg));
}
};
let opts = RouteOpts {
systems: hops,
range: Some(range),
file_path: PathBuf::from(path),
precomp_file: precomp.map(PathBuf::from),
callback: Box::new(callback_wrapped),
mode,
factor: greedyness,
precompute: false,
permute,
keep_first,
keep_last,
primary,
workers: num_workers,
};
match route(opts) {
Ok(Some(route)) => {
let hops = route.iter().map(|hop| {
let pos = PyList::new(py, hop.pos.iter());
let elem = PyDict::new(py);
elem.set_item("star_type", hop.star_type.clone()).unwrap();
elem.set_item("system", hop.system.clone()).unwrap();
elem.set_item("body", hop.body.clone()).unwrap();
elem.set_item("distance", hop.distance).unwrap();
elem.set_item("pos", pos).unwrap();
elem
});
let lst = PyList::new(py, hops);
Ok(lst.to_object(py))
}
Ok(None) => Ok(py.None()),
Err(e) => Err(PyErr::new::<ValueError, _>(e)),
}
}
*/

69
rust/src/mmap_csv.rs Normal file
View File

@ -0,0 +1,69 @@
use crate::common::{EdLrrError, EdLrrResult, System};
use crate::info;
use csv_core::{ReadFieldResult, Reader};
use memmap::Mmap;
use std::collections::HashMap;
use std::fs::File;
use std::path::Path;
pub fn mmap_csv(path: &Path, query: Vec<String>) -> Result<HashMap<String, Option<u32>>, String> {
let file = File::open(path).map_err(|e| e.to_string())?;
let mm = unsafe { Mmap::map(&file) }.map_err(|e| e.to_string())?;
let mut best = query
.iter()
.map(|s| (s, (s.as_bytes(), usize::MAX, u32::MAX)))
.collect::<Vec<(&String, (_, usize, u32))>>();
let t_start = std::time::Instant::now();
let dist = eddie::slice::DamerauLevenshtein::new();
let mut row = 0;
{
let mut data = &mm[..];
let mut rdr = Reader::new();
let mut field = [0; 1024];
let mut fieldidx = 0;
loop {
let (result, nread, nwrite) = rdr.read_field(data, &mut field);
data = &data[nread..];
let field = &field[..nwrite];
match result {
ReadFieldResult::InputEmpty => {}
ReadFieldResult::OutputFull => {
return Err("Encountered field larget than 1024 bytes!".to_string());
}
ReadFieldResult::Field { record_end } => {
if fieldidx == 1 {
for (_, (name_b, best_dist, id)) in best.iter_mut() {
let d = dist.distance(name_b, field);
if d < *best_dist {
*best_dist = d;
*id = row;
}
}
}
if record_end {
fieldidx = 0;
row += 1;
} else {
fieldidx += 1;
}
}
// This case happens when the CSV reader has successfully exhausted
// all input.
ReadFieldResult::End => {
break;
}
}
}
}
let search_result = best
.drain(..)
.map(|(query_name, (_, _, idx))| (query_name.clone(), Some(idx)))
.collect::<HashMap<String, Option<u32>>>();
let rate = (row as f64) / t_start.elapsed().as_secs_f64();
info!(
"Took: {:.2?}, {:.2} systems/second",
t_start.elapsed(),
rate
);
Ok(search_result)
}

15
rust/src/profiling.rs Normal file
View File

@ -0,0 +1,15 @@
#![cfg(feature = "profiling")]
use tracing::subscriber::set_global_default;
pub use tracing::{debug, error, info, span, trace, warn, Level};
pub use tracing::{debug_span, error_span, info_span, trace_span, warn_span};
use tracing_chrome::ChromeLayerBuilder;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::Registry;
use tracing_tracy::TracyLayer;
pub use tracy_client::ProfiledAllocator;
pub fn init() {
let (chrome_layer, _guard) = ChromeLayerBuilder::new().build();
let subscriber = Registry::default().with(chrome_layer);
set_global_default(subscriber).expect("setting default subscriber failed");
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,56 @@
use crate::common::TreeNode;
use crate::route::{Router, SearchState};
use fnv::FnvHashMap;
trait SearchAlgoImpl<State = (), Weight: Ord = ()> {
fn get_weight(&mut self, systems: &TreeNode, router: &Router) -> Option<Weight>;
fn get_neighbors(
&mut self,
system: &TreeNode,
router: &Router,
range: f32,
) -> Vec<(Weight, TreeNode)> {
let mut ret = vec![];
for nb in router.neighbours(system, range) {
if let Some(w) = self.get_weight(nb, router) {
ret.push((w, *nb));
}
}
return ret;
}
}
struct SearchAlgo<'a> {
algo: Box<dyn SearchAlgoImpl>,
prev: FnvHashMap<u32, u32>,
state: Option<SearchState>,
router: &'a Router,
}
struct BFS(usize);
impl SearchAlgoImpl for BFS {
fn get_weight(&mut self, _system: &TreeNode, _router: &Router) -> Option<()> {
return Some(());
}
}
impl<'a> SearchAlgo<'a> {
fn new(router: &'a Router, algo: Box<dyn SearchAlgoImpl>) -> Self {
Self {
algo,
prev: FnvHashMap::default(),
state: None,
router,
}
}
fn test(&mut self) {
// self.algo.get_neighbors
}
}
/*
a = 1 - acos(dot(u/Length(u),v/Length(v)))/PI
*/

View File

@ -1,5 +1,7 @@
//! Ship fuel consumption and jump range calculations
use crate::common::get_fsd_booster_info;
use crate::journal::*;
use eyre::Result;
use pyo3::conversion::ToPyObject;
use pyo3::prelude::*;
use pyo3::types::PyDict;
@ -10,21 +12,25 @@ use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::PathBuf;
/// Frame Shift Drive information
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FSD {
/// Rating
pub rating_val: f32,
/// Class
pub class_val: f32,
/// Optimized Mass
pub opt_mass: f32,
/// Max fuel per jump
pub max_fuel: f32,
/// Boost factor
pub boost: f32,
/// Guardian booster bonus range
pub guardian_booster: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Ship {
pub name: String,
pub ident: String,
pub ship_type: String,
pub base_mass: f32,
pub fuel_mass: f32,
pub fuel_capacity: f32,
@ -33,9 +39,6 @@ pub struct Ship {
impl Ship {
pub fn new(
name: String,
ident: String,
ship_type: String,
base_mass: f32,
fuel_mass: f32,
fuel_capacity: f32,
@ -57,15 +60,12 @@ impl Ship {
if fsd_type.1 < 2 || fsd_type.1 > 8 {
return Err(format!("Invalid class: {}", fsd_type.1));
};
if guardian_booster!=0 {
return Err("Guardian booster not yet implemented!".to_owned())
if guardian_booster != 0 {
return Err("Guardian booster not yet implemented!".to_owned());
}
let ret = Self {
name,
ident,
ship_type,
fuel_capacity,
fuel_mass,
base_mass,
@ -81,8 +81,8 @@ impl Ship {
Ok(ret)
}
pub fn new_from_json(data: &str) -> Result<Self, String> {
match serde_json::from_str::<Event>(&data) {
pub fn new_from_json(data: &str) -> Result<(String, Ship), String> {
match serde_json::from_str::<Event>(data) {
Ok(Event {
event: EventData::Unknown,
}) => {
@ -101,7 +101,7 @@ impl Ship {
};
}
pub fn new_from_journal() -> Result<HashMap<String, Self>, String> {
pub fn new_from_journal() -> Result<HashMap<String, Ship>, String> {
let mut ret = HashMap::new();
let re = Regex::new(r"^Journal\.\d{12}\.\d{2}\.log$").unwrap();
let mut journals: Vec<PathBuf> = Vec::new();
@ -110,12 +110,10 @@ impl Ship {
userprofile.push("Frontier Developments");
userprofile.push("Elite Dangerous");
if let Ok(iter) = userprofile.read_dir() {
for entry in iter {
if let Ok(entry) = entry {
if re.is_match(&entry.file_name().to_string_lossy()) {
journals.push(entry.path());
};
}
for entry in iter.flatten() {
if re.is_match(&entry.file_name().to_string_lossy()) {
journals.push(entry.path());
};
}
}
journals.sort();
@ -133,16 +131,7 @@ impl Ship {
}) => {}
Ok(ev) => {
if let Some(loadout) = ev.get_loadout() {
let mut ship = loadout.try_into_ship()?;
if ship.name == "" {
ship.name = "<NO NAME>".to_owned();
}
let key = format!(
"[{}] {} ({})",
ship.ident,
ship.name,
ship.ship_type.to_ascii_lowercase()
);
let (key, ship) = loadout.try_into_ship()?;
ret.insert(key, ship);
}
}
@ -179,7 +168,7 @@ impl Ship {
Some(cost)
}
fn jump_range(&self, fuel: f32, booster: bool) -> f32 {
pub fn jump_range(&self, fuel: f32, booster: bool) -> f32 {
let mass = self.base_mass + fuel;
let mut fuel = self.fsd.max_fuel.min(fuel);
if booster {
@ -198,6 +187,10 @@ impl Ship {
return self.jump_range(self.fuel_mass, true);
}
pub fn full_range(&self) -> f32 {
return self.jump_range(self.fuel_capacity, true);
}
fn boost_fuel_mult(&self) -> f32 {
if self.fsd.guardian_booster == 0.0 {
return 1.0;
@ -208,6 +201,21 @@ impl Ship {
return ((base_range + self.fsd.guardian_booster) / base_range).powf(self.fsd.class_val);
}
pub fn fuel_cost_for_jump(&self, fuel_mass: f32, dist: f32, boost: f32) -> Option<(f32, f32)> {
if dist == 0.0 {
return Some((0.0, 0.0));
}
let mass = self.base_mass + fuel_mass;
let opt_mass = self.fsd.opt_mass * boost;
let base_cost = (dist * mass) / opt_mass;
let fuel_cost = (self.fsd.rating_val * 0.001 * base_cost.powf(self.fsd.class_val))
/ self.boost_fuel_mult();
if fuel_cost > self.fsd.max_fuel || fuel_cost > fuel_mass {
return None;
};
return Some((fuel_cost, fuel_mass - fuel_cost));
}
pub fn fuel_cost(&self, d: f32) -> f32 {
if d == 0.0 {
return 0.0;
@ -220,29 +228,6 @@ impl Ship {
}
}
/*
#[derive(Debug,Clone, Serialize, Deserialize, ToPyObject)]
pub struct FSD {
pub rating_val: f32,
pub class_val: f32,
pub opt_mass: f32,
pub max_fuel: f32,
pub boost: f32,
pub guardian_booster: f32,
}
#[derive(Debug,Clone, Serialize, Deserialize, ToPyObject)]
pub struct Ship {
pub name: String,
pub ident: String,
pub ship_type: String,
pub base_mass: f32,
pub fuel_mass: f32,
pub fuel_capacity: f32,
pub fsd: FSD,
}
*/
impl FSD {
pub fn to_object(&self, py: Python) -> PyResult<PyObject> {
let elem = PyDict::new(py);
@ -259,9 +244,6 @@ impl FSD {
impl Ship {
pub fn to_object(&self, py: Python) -> PyResult<PyObject> {
let elem = PyDict::new(py);
elem.set_item("name", self.name.clone())?;
elem.set_item("ident", self.ident.clone())?;
elem.set_item("ship_type", self.ship_type.clone())?;
elem.set_item("base_mass", self.base_mass)?;
elem.set_item("fuel_mass", self.fuel_mass)?;
elem.set_item("fuel_capacity", self.fuel_capacity)?;

69
rust/tests/dot_impls.rs Normal file
View File

@ -0,0 +1,69 @@
#[inline(always)]
fn veclen(v: &[f32; 3]) -> f32 {
(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt()
}
#[inline(always)]
fn dist2(p1: &[f32; 3], p2: &[f32; 3]) -> f32 {
let dx = p1[0] - p2[0];
let dy = p1[1] - p2[1];
let dz = p1[2] - p2[2];
dx * dx + dy * dy + dz * dz
}
#[inline(always)]
fn dist(p1: &[f32; 3], p2: &[f32; 3]) -> f32 {
dist2(p1, p2).sqrt()
}
#[inline(always)]
fn distm(p1: &[f32; 3], p2: &[f32; 3]) -> f32 {
let dx = (p1[0] - p2[0]).abs();
let dy = (p1[1] - p2[1]).abs();
let dz = (p1[2] - p2[2]).abs();
dx + dy + dz
}
/// Dot product (cosine of angle) between two 3D vectors
#[inline(always)]
pub fn ndot_vec_dist(u: &[f32; 3], v: &[f32; 3]) -> f32 {
let z: [f32; 3] = [0.0; 3];
let lm = dist(u, &z) * dist(v, &z);
((u[0] * v[0]) + (u[1] * v[1]) + (u[2] * v[2])) / lm
}
/// Dot product (cosine of angle) between two 3D vectors
#[inline(always)]
pub fn ndot_vec_len(u: &[f32; 3], v: &[f32; 3]) -> f32 {
let lm = veclen(u) * veclen(v);
((u[0] * v[0]) + (u[1] * v[1]) + (u[2] * v[2])) / lm
}
#[inline(always)]
pub fn ndot_iter(u: &[f32; 3], v: &[f32; 3]) -> f32 {
let l_1: f32 = u.iter().map(|e| e * e).sum();
let l_2: f32 = v.iter().map(|e| e * e).sum();
let lm = (l_1 * l_2).sqrt();
let mut ret = 0.0;
for (a, b) in u.iter().zip(v.iter()) {
ret += a * b;
}
ret / lm
}
#[cfg(test)]
mod dot_impl_tests {
#[test]
fn test_dot_impls() {
use super::*;
let v1 = [1.0, 2.0, 3.0];
let v2 = [4.0, 5.0, 6.0];
let d1 = ndot_vec_dist(&v1, &v2);
let d2 = ndot_vec_len(&v1, &v2);
let d3 = ndot_iter(&v1, &v2);
assert!((d1 - d2) < 0.01);
assert!((d2 - d3) < 0.01);
assert!((d3 - d1) < 0.01);
}
}

170
setup.py
View File

@ -1,76 +1,82 @@
# -*- coding: utf-8 -*-
from setuptools import find_packages, setup
from setuptools_rust import Binding, RustExtension, Strip
with open('README.md', 'r') as fh:
import os
with open("README.md", "r") as fh:
long_description = fh.read()
extras_require = {
'build': ['pyinstaller', 'pywin32'],
'test': [
'pytest',
'pytest-cov',
'pytest-dependency',
'pytest-benchmark[histogram]',
'pytest-metadata',
'pytest-flake8',
'pytest-flask',
'pytest-mock',
'pytest-flask-sqlalchemy',
'pytest-steps',
'pytest-xdist',
'flake8-bugbear',
'flake8-comprehensions',
'cohesion',
'hypothesis',
'flaky'
"build": ["pyinstaller", "pywin32"],
"test": [
"pytest",
"pytest-cov",
"pytest-dependency",
"pytest-benchmark[histogram]",
"pytest-metadata",
"pytest-flake8",
"pytest-flask",
"pytest-mock",
"pytest-flask-sqlalchemy",
"pytest-steps",
"pytest-xdist",
"flake8-bugbear",
"flake8-comprehensions",
"cohesion",
"hypothesis",
"flaky",
],
'dev': [
"dev": [
'black; python_version >= "3.6"',
'jinja2',
'tsp',
'flake8',
'flake8-bugbear',
'flake8-comprehensions',
'cohesion',
'pre-commit',
'ipython',
'flask-konch',
'setuptools_rust'
"jinja2",
"tsp",
"flake8",
"flake8-bugbear",
"flake8-comprehensions",
"cohesion",
"pre-commit",
"ipython",
"flask-konch",
"setuptools_rust",
],
'gui': ['PyQt5', 'pyperclip'],
'web': [
'flask',
'gevent',
'webargs',
'flask-executor',
'flask-wtf',
'flask-user',
'flask-debugtoolbar',
'flask-bootstrap4',
'flask-sqlalchemy',
'flask-nav',
'flask-admin',
'sqlalchemy_utils[password]',
'python-dotenv',
"gui": ["PyQt5", "pyperclip"],
"web": [
"flask",
"gevent",
"webargs",
"flask-executor",
"flask-wtf",
"flask-user",
"flask-debugtoolbar",
"flask-bootstrap4",
"flask-sqlalchemy",
"flask-nav",
"flask-admin",
"sqlalchemy_utils[password]",
"python-dotenv",
],
}
extras_require['all'] = sorted(set(sum(extras_require.values(), [])))
extras_require["all"] = sorted(set(sum(extras_require.values(), [])))
# os.environ["RUSTC_WRAPPER"]='"{}" /c echo'.format(os.environ['COMSPEC'])
setup(
use_scm_version={'write_to': '__version__.py'},
name='ed_lrr_gui',
author='Daniel Seiller',
author_email='earthnuker@gmail.com',
description='Elite: Dangerous long range route plotter',
use_scm_version={"write_to": "__version__.py"},
name="ed_lrr_gui",
author="Daniel Seiller",
author_email="earthnuker@gmail.com",
description="Elite: Dangerous long range route plotter",
long_description=long_description,
long_description_content_type='text/markdown',
url='https://gitlab.com/Earthnuker/ed_lrr/-/tree/pyqt_gui',
long_description_content_type="text/markdown",
url="https://gitlab.com/Earthnuker/ed_lrr/-/tree/pyqt_gui",
rust_extensions=[
RustExtension(
'_ed_lrr',
path='rust/Cargo.toml',
"_ed_lrr",
path="rust/Cargo.toml",
binding=Binding.PyO3,
strip=Strip.No,
rustc_flags=["--emit=asm"],
# features=["profiling"],
debug=False,
native=True,
quiet=True,
@ -78,38 +84,38 @@ setup(
],
packages=find_packages(),
entry_points={
'console_scripts': ['ed_lrr = ed_lrr_gui.__main__:main'],
'gui_scripts': ['ed_lrr_gui = ed_lrr_gui.__main__:gui_main']
"console_scripts": ["ed_lrr = ed_lrr_gui.__main__:main"],
"gui_scripts": ["ed_lrr_gui = ed_lrr_gui.__main__:gui_main"],
},
install_requires=[
'appdirs',
'PyYAML',
'requests',
'python-dateutil',
'click',
'tqdm',
'click-default-group',
'profig',
'ujson',
'colorama',
'svgwrite',
"appdirs",
"PyYAML",
"requests",
"python-dateutil",
"click",
"tqdm",
"click-default-group",
"profig",
"ujson",
"colorama",
"svgwrite",
"coloredlogs",
],
setup_requires=['setuptools', 'setuptools-rust',
'setuptools-scm', 'wheel'],
dependency_links=['https://github.com/Nuitka/Nuitka/archive/develop.zip'],
setup_requires=["setuptools", "setuptools-rust", "setuptools-scm", "wheel"],
dependency_links=["https://github.com/Nuitka/Nuitka/archive/develop.zip"],
extras_require=extras_require,
classifiers=[
'License :: OSI Approved :: MIT License',
'Programming Language :: Rust',
'Programming Language :: Python',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: Implementation :: CPython',
'Operating System :: Windows',
'Operating System :: Linux',
"License :: OSI Approved :: MIT License",
"Programming Language :: Rust",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: Implementation :: CPython",
"Operating System :: Windows",
"Operating System :: Linux",
],
include_package_data=True,
zip_safe=False,

45
test_route.py Normal file
View File

@ -0,0 +1,45 @@
import sys
import logging
import coloredlogs
from datetime import timedelta
coloredlogs.DEFAULT_FIELD_STYLES["delta"] = {"color": "green"}
coloredlogs.DEFAULT_FIELD_STYLES["levelname"] = {"color": "yellow"}
class DeltaTimeFormatter(coloredlogs.ColoredFormatter):
def format(self, record):
seconds = record.relativeCreated / 1000
duration = timedelta(seconds=seconds)
record.delta = str(duration)
return super().format(record)
coloredlogs.ColoredFormatter = DeltaTimeFormatter
logfmt = " | ".join(
["[%(delta)s] %(levelname)s", "%(name)s:%(pathname)s:%(lineno)s", "%(message)s"]
)
loglevel = "info"
numeric_level = getattr(logging, loglevel.upper(), None)
if not isinstance(numeric_level, int):
raise ValueError("Invalid log level: %s" % loglevel)
coloredlogs.install(level=numeric_level, fmt=logfmt)
sys.path.append(".")
import _ed_lrr
ships = _ed_lrr.PyShip.from_journal()
r = _ed_lrr.PyRouter(print)
r.load("stars.csv")
ship = max(ships.values(), key=lambda s: s.max_range)
system_names = ["Sol", "Colonia"]
systems = r.resolve(*system_names)
sys_ids = {k: v["id"] for k, v in systems.items()}
route = r.route(
[sys_ids[system_names[0]], sys_ids[system_names[1]]],
7.84,
{"mode": "bfs","greedyness":0.0},
)
for n,s in enumerate(route,1):
print(n,s)

289
tests/data/ships/base.json Normal file
View File

@ -0,0 +1,289 @@
{
"timestamp": "2019-09-25T21:29:51Z",
"event": "Loadout",
"Ship": "asp",
"ShipID": 0,
"ShipName": "Nightmaregreen_N",
"ShipIdent": "NMGR_N",
"HullValue": 6144793,
"ModulesValue": 33042643,
"HullHealth": 1.000000,
"UnladenMass": 347.200012,
"CargoCapacity": 0,
"MaxJumpRange": 56.372398,
"FuelCapacity": {
"Main": 64.000000,
"Reserve": 0.630000
},
"Rebuy": 1959374,
"Modules": [
{
"Slot": "ShipCockpit",
"Item": "asp_cockpit",
"On": true,
"Priority": 1,
"Health": 1.000000
},
{
"Slot": "CargoHatch",
"Item": "modularcargobaydoor",
"On": true,
"Priority": 0,
"Health": 1.000000
},
{
"Slot": "TinyHardpoint1",
"Item": "hpt_heatsinklauncher_turret_tiny",
"On": true,
"Priority": 0,
"AmmoInClip": 1,
"AmmoInHopper": 2,
"Health": 1.000000
},
{
"Slot": "TinyHardpoint2",
"Item": "hpt_heatsinklauncher_turret_tiny",
"On": true,
"Priority": 0,
"AmmoInClip": 1,
"AmmoInHopper": 2,
"Health": 1.000000
},
{
"Slot": "TinyHardpoint3",
"Item": "hpt_heatsinklauncher_turret_tiny",
"On": true,
"Priority": 0,
"AmmoInClip": 1,
"AmmoInHopper": 2,
"Health": 1.000000
},
{
"Slot": "TinyHardpoint4",
"Item": "hpt_heatsinklauncher_turret_tiny",
"On": true,
"Priority": 0,
"AmmoInClip": 1,
"AmmoInHopper": 2,
"Health": 1.000000
},
{
"Slot": "PaintJob",
"Item": "paintjob_asp_operator_red",
"On": true,
"Priority": 1,
"Health": 1.000000
},
{
"Slot": "Armour",
"Item": "asp_armour_grade1",
"On": true,
"Priority": 1,
"Health": 1.000000
},
{
"Slot": "PowerPlant",
"Item": "int_powerplant_size5_class2",
"On": true,
"Priority": 1,
"Health": 1.000000
},
{
"Slot": "MainEngines",
"Item": "int_engine_size4_class2",
"On": true,
"Priority": 0,
"Health": 1.000000
},
{
"Slot": "FrameShiftDrive",
"Item": "int_hyperdrive_size5_class5",
"On": true,
"Priority": 0,
"Health": 1.000000,
"Engineering": {
"Engineer": "Felicity Farseer",
"EngineerID": 300100,
"BlueprintID": 128673694,
"BlueprintName": "FSD_LongRange",
"Level": 5,
"Quality": 1.000000,
"ExperimentalEffect": "special_fsd_heavy",
"ExperimentalEffect_Localised": "Mass Manager",
"Modifiers": [
{
"Label": "Mass",
"Value": 26.000000,
"OriginalValue": 20.000000,
"LessIsGood": 1
},
{
"Label": "Integrity",
"Value": 93.840004,
"OriginalValue": 120.000000,
"LessIsGood": 0
},
{
"Label": "PowerDraw",
"Value": 0.690000,
"OriginalValue": 0.600000,
"LessIsGood": 1
},
{
"Label": "FSDOptimalMass",
"Value": 1692.599976,
"OriginalValue": 1050.000000,
"LessIsGood": 0
}
]
}
},
{
"Slot": "LifeSupport",
"Item": "int_lifesupport_size4_class2",
"On": true,
"Priority": 0,
"Health": 1.000000
},
{
"Slot": "PowerDistributor",
"Item": "int_powerdistributor_size4_class2",
"On": true,
"Priority": 0,
"Health": 1.000000
},
{
"Slot": "Radar",
"Item": "int_sensors_size5_class2",
"On": true,
"Priority": 0,
"Health": 1.000000
},
{
"Slot": "FuelTank",
"Item": "int_fueltank_size5_class3",
"On": true,
"Priority": 1,
"Health": 1.000000
},
{
"Slot": "Decal1",
"Item": "decal_explorer_starblazer",
"On": true,
"Priority": 1,
"Health": 1.000000
},
{
"Slot": "Decal2",
"Item": "decal_explorer_starblazer",
"On": true,
"Priority": 1,
"Health": 1.000000
},
{
"Slot": "Decal3",
"Item": "decal_explorer_starblazer",
"On": true,
"Priority": 1,
"Health": 1.000000
},
{
"Slot": "ShipName0",
"Item": "nameplate_shipname_white",
"On": true,
"Priority": 1,
"Health": 1.000000
},
{
"Slot": "ShipName1",
"Item": "nameplate_shipname_white",
"On": true,
"Priority": 1,
"Health": 1.000000
},
{
"Slot": "ShipID0",
"Item": "nameplate_shipid_white",
"On": true,
"Priority": 1,
"Health": 1.000000
},
{
"Slot": "ShipID1",
"Item": "nameplate_shipid_white",
"On": true,
"Priority": 1,
"Health": 1.000000
},
{
"Slot": "Slot01_Size6",
"Item": "int_fuelscoop_size6_class5",
"On": true,
"Priority": 0,
"Health": 1.000000
},
{
"Slot": "Slot02_Size5",
"Item": "int_fueltank_size5_class3",
"On": true,
"Priority": 1,
"Health": 1.000000
},
{
"Slot": "Slot03_Size3",
"Item": "int_repairer_size3_class5",
"On": false,
"Priority": 0,
"Health": 1.000000
},
{
"Slot": "Slot04_Size3",
"Item": "int_shieldgenerator_size3_class2",
"On": true,
"Priority": 0,
"Health": 1.000000
},
{
"Slot": "Slot05_Size3",
"Item": "int_buggybay_size2_class2",
"On": true,
"Priority": 0,
"Health": 1.000000
},
{
"Slot": "Slot06_Size2",
"Item": "int_detailedsurfacescanner_tiny",
"On": true,
"Priority": 0,
"Health": 1.000000
},
{
"Slot": "Slot07_Size2",
"Item": "int_dockingcomputer_standard",
"On": true,
"Priority": 0,
"Health": 1.000000
},
{
"Slot": "Slot08_Size1",
"Item": "int_supercruiseassist",
"On": true,
"Priority": 0,
"Health": 1.000000
},
{
"Slot": "PlanetaryApproachSuite",
"Item": "int_planetapproachsuite",
"On": true,
"Priority": 1,
"Health": 1.000000
},
{
"Slot": "VesselVoice",
"Item": "voicepack_eden",
"On": true,
"Priority": 1,
"Health": 1.000000
}
]
}

View File

@ -0,0 +1,193 @@
{
"timestamp": "2019-09-25T21:29:51Z",
"event": "Loadout",
"Ship": "asp",
"ShipName": "Nightmaregreen_G",
"ShipIdent": "NMGR_G",
"HullValue": 6144793,
"ModulesValue": 33181682,
"UnladenMass": 348.500061,
"CargoCapacity": 0,
"MaxJumpRange": 60.164637,
"FuelCapacity": {
"Main": 64,
"Reserve": 0.63
},
"Rebuy": 1966323,
"Modules": [
{
"Slot": "CargoHatch",
"Item": "modularcargobaydoor",
"On": true,
"Priority": 0
},
{
"Slot": "TinyHardpoint1",
"Item": "hpt_heatsinklauncher_turret_tiny",
"On": true,
"Priority": 0,
"Value": 3071
},
{
"Slot": "TinyHardpoint2",
"Item": "hpt_heatsinklauncher_turret_tiny",
"On": true,
"Priority": 0,
"Value": 3071
},
{
"Slot": "TinyHardpoint3",
"Item": "hpt_heatsinklauncher_turret_tiny",
"On": true,
"Priority": 0,
"Value": 3071
},
{
"Slot": "TinyHardpoint4",
"Item": "hpt_heatsinklauncher_turret_tiny",
"On": true,
"Priority": 0,
"Value": 3071
},
{
"Slot": "Armour",
"Item": "asp_armour_grade1",
"On": true,
"Priority": 1,
"Value": 0
},
{
"Slot": "PowerPlant",
"Item": "int_powerplant_size5_class2",
"On": true,
"Priority": 1,
"Value": 140523
},
{
"Slot": "MainEngines",
"Item": "int_engine_size4_class2",
"On": true,
"Priority": 0,
"Value": 52325
},
{
"Slot": "FrameShiftDrive",
"Item": "int_hyperdrive_size5_class5",
"On": true,
"Priority": 0,
"Value": 4478716,
"Engineering": {
"BlueprintName": "FSD_LongRange",
"Level": 5,
"Quality": 1,
"ExperimentalEffect": "special_fsd_heavy",
"Modifiers": [
{
"Label": "Mass",
"Value": 26.000061,
"OriginalValue": 20
},
{
"Label": "Integrity",
"Value": 93.839832,
"OriginalValue": 120
},
{
"Label": "PowerDraw",
"Value": 0.690001,
"OriginalValue": 0.6
},
{
"Label": "FSDOptimalMass",
"Value": 1692.58667,
"OriginalValue": 1050
}
]
}
},
{
"Slot": "LifeSupport",
"Item": "int_lifesupport_size4_class2",
"On": true,
"Priority": 0,
"Value": 24895
},
{
"Slot": "PowerDistributor",
"Item": "int_powerdistributor_size4_class2",
"On": true,
"Priority": 0,
"Value": 24895
},
{
"Slot": "Radar",
"Item": "int_sensors_size5_class2",
"On": true,
"Priority": 0,
"Value": 69709
},
{
"Slot": "FuelTank",
"Item": "int_fueltank_size5_class3",
"On": true,
"Priority": 1,
"Value": 85776
},
{
"Slot": "Slot01_Size6",
"Item": "int_fuelscoop_size6_class5",
"On": true,
"Priority": 0,
"Value": 25240068
},
{
"Slot": "Slot02_Size5",
"Item": "int_fueltank_size5_class3",
"On": true,
"Priority": 1,
"Value": 85776
},
{
"Slot": "Slot03_Size3",
"Item": "int_repairer_size3_class5",
"On": false,
"Priority": 0,
"Value": 2302911
},
{
"Slot": "Slot04_Size3",
"Item": "int_shieldgenerator_size3_class2",
"On": true,
"Priority": 0,
"Value": 16506
},
{
"Slot": "Slot05_Size3",
"Item": "int_buggybay_size2_class2",
"On": true,
"Priority": 0,
"Value": 18954
},
{
"Slot": "Slot06_Size2",
"Item": "int_detailedsurfacescanner_tiny",
"On": true,
"Priority": 0,
"Value": 219375
},
{
"Slot": "Slot07_Size2",
"Item": "int_dockingcomputer_standard",
"On": true,
"Priority": 0,
"Value": 3949
},
{
"Slot": "Slot08_Size1",
"Item": "int_guardianfsdbooster_size1",
"On": true,
"Priority": 0,
"Value": 405020
}
]
}

View File

@ -79,18 +79,18 @@ class Test_PyRouter(object): # noqa: H601
err = "Failed to resolve {}".format(name)
assert name in resolved_systems, err
@pytest.mark.dependency(depends=["Test_ED_LRR::test_load_and_resolve"])
@pytest.mark.dependency(depends=["Test_PyRouter::test_load_and_resolve"])
@flaky(max_runs=10, min_passes=5)
@pytest.mark.parametrize(
"greedyness", greedyness, ids=lambda v: "greedyness:{}".format(v)
)
@flaky(max_runs=10, min_passes=5)
def test_zero_range_fails(self, py_router, greedyness):
r, resolved_systems = py_router
waypoints = random.sample(list(resolved_systems.values()), k=2)
err = pytest.raises(RuntimeError, r.route, waypoints, 0, greedyness)
err.match(r"No route from .* to .* found!")
@pytest.mark.dependency(depends=["Test_ED_LRR::test_load_and_resolve"])
@pytest.mark.dependency(depends=["Test_PyRouter::test_load_and_resolve"])
@flaky(max_runs=10, min_passes=2)
@pytest.mark.parametrize("workers", n_workers, ids=idf("workers"))
@pytest.mark.parametrize("jump_range", ranges, ids=idf("range"))