├── .gitignore ├── LICENSE ├── data_export.sh ├── demo_pics ├── ui connections.png ├── ui dyad.png ├── ui full graph.png ├── ui main.png ├── ui merge.png ├── ui nodelist.png ├── ui nodes.png ├── ui trailing.png ├── ui triad.png ├── ui viz classical 1.png ├── ui viz classical 2.png ├── ui viz classical 3.png ├── ui viz full mintree.png ├── ui viz luxury 1.png ├── ui viz luxury mintree.png └── ui-main.png ├── ideas └── viz.md.html ├── readme.md ├── requirements.txt ├── setup.py └── sursis ├── __init__.py ├── app.py ├── backend ├── data.sqlite ├── db.py ├── db_init.py ├── graph_physics.py ├── physics.py └── stats.py ├── data.sqlite ├── dialogs.py ├── graph_views.py ├── initialize_script.py ├── ui_elems.py └── viz.py /.gitignore: -------------------------------------------------------------------------------- 1 | env 2 | env/* 3 | *.json 4 | *.bz2 5 | *.tar.bz 6 | *.pyc 7 | *.sqlite 8 | app.log 9 | *.pyc 10 | *.old.py 11 | edges.txt 12 | nodes.txt 13 | named_edges.txt 14 | cache/* 15 | backup/* 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2022 Diego Navarro 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /data_export.sh: -------------------------------------------------------------------------------- 1 | sqlite3 data.sqlite "select * from nodes" > nodes.txt 2 | sqlite3 data.sqlite "select * from edges" > edges.txt 3 | sqlite3 data.sqlite "select * from edges" > named_edges.txt 4 | -------------------------------------------------------------------------------- /demo_pics/ui connections.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asemic-horizon/sursis/c7b8244d26f43eb67642146991622cbaf84046f4/demo_pics/ui connections.png -------------------------------------------------------------------------------- /demo_pics/ui dyad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asemic-horizon/sursis/c7b8244d26f43eb67642146991622cbaf84046f4/demo_pics/ui dyad.png -------------------------------------------------------------------------------- /demo_pics/ui full graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asemic-horizon/sursis/c7b8244d26f43eb67642146991622cbaf84046f4/demo_pics/ui full graph.png -------------------------------------------------------------------------------- /demo_pics/ui main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asemic-horizon/sursis/c7b8244d26f43eb67642146991622cbaf84046f4/demo_pics/ui main.png -------------------------------------------------------------------------------- /demo_pics/ui merge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asemic-horizon/sursis/c7b8244d26f43eb67642146991622cbaf84046f4/demo_pics/ui merge.png -------------------------------------------------------------------------------- /demo_pics/ui nodelist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asemic-horizon/sursis/c7b8244d26f43eb67642146991622cbaf84046f4/demo_pics/ui nodelist.png -------------------------------------------------------------------------------- /demo_pics/ui nodes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asemic-horizon/sursis/c7b8244d26f43eb67642146991622cbaf84046f4/demo_pics/ui nodes.png -------------------------------------------------------------------------------- /demo_pics/ui trailing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asemic-horizon/sursis/c7b8244d26f43eb67642146991622cbaf84046f4/demo_pics/ui trailing.png -------------------------------------------------------------------------------- /demo_pics/ui triad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asemic-horizon/sursis/c7b8244d26f43eb67642146991622cbaf84046f4/demo_pics/ui triad.png -------------------------------------------------------------------------------- /demo_pics/ui viz classical 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asemic-horizon/sursis/c7b8244d26f43eb67642146991622cbaf84046f4/demo_pics/ui viz classical 1.png -------------------------------------------------------------------------------- /demo_pics/ui viz classical 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asemic-horizon/sursis/c7b8244d26f43eb67642146991622cbaf84046f4/demo_pics/ui viz classical 2.png -------------------------------------------------------------------------------- /demo_pics/ui viz classical 3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asemic-horizon/sursis/c7b8244d26f43eb67642146991622cbaf84046f4/demo_pics/ui viz classical 3.png -------------------------------------------------------------------------------- /demo_pics/ui viz full mintree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asemic-horizon/sursis/c7b8244d26f43eb67642146991622cbaf84046f4/demo_pics/ui viz full mintree.png -------------------------------------------------------------------------------- /demo_pics/ui viz luxury 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asemic-horizon/sursis/c7b8244d26f43eb67642146991622cbaf84046f4/demo_pics/ui viz luxury 1.png -------------------------------------------------------------------------------- /demo_pics/ui viz luxury mintree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asemic-horizon/sursis/c7b8244d26f43eb67642146991622cbaf84046f4/demo_pics/ui viz luxury mintree.png -------------------------------------------------------------------------------- /demo_pics/ui-main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asemic-horizon/sursis/c7b8244d26f43eb67642146991622cbaf84046f4/demo_pics/ui-main.png -------------------------------------------------------------------------------- /ideas/viz.md.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | **Calculus on graphs (for poets)** 5 | asemic horizon 6 | 7 | $$\newcommand{\re}{\mathbb R} $$ 8 | 9 | 10 | Intended audience 11 | ================= 12 | 13 | 14 | Bare essentials 15 | ================ 16 | 17 | 18 | Summation 19 | ---------- 20 | 21 | \begin{equation} 22 | \sum_{i=1}^n a_i = a_1 + a_2 + \cdots + a_{n-1} + a_n 23 | \end{equation} 24 | 25 | 26 | \begin{equation} 27 | \sum_{i=1}^n a_i b_i = a_1 b_1 + a_2 b_2 + \cdots + a_{n-1} b_{n-1} + a_n b_n 28 | \end{equation} 29 | 30 | \begin{equation} 31 | \sum_{i=1}^n a_{ij} = a_{1j} + a_{2j} + \cdots + a_{(n-1)j} + a_{nj} 32 | \end{equation} 33 | 34 | 35 | Matrices 36 | --------- 37 | 38 | \begin{equation} 39 | (Av)_j = \sum_i A_{ij} v_i 40 | \end{equation} 41 | 42 | 43 | \begin{equation} 44 | \begin{bmatrix} 45 | 3 & -2 \\ 1 & 1 46 | \end{bmatrix} 47 | \begin{bmatrix} 48 | x \\ y 49 | \end{bmatrix} 50 | = 51 | \begin{bmatrix} 52 | 3x - 2y \\ x + y 53 | \end{bmatrix} 54 | 55 | \end{equation} 56 | 57 | Linear transforms 58 | ------------------ 59 | 60 | 61 | Bilinear forms 62 | -------------- 63 | 64 | \begin{equation} 65 | B(u,v) = u^\top B v 66 | \end{equation} 67 | 68 | 69 | \begin{equation} 70 | Q(u) = u^\top B u 71 | \end{equation} 72 | 73 | 74 | \begin{equation} 75 | \begin{bmatrix} 76 | x & y 77 | \end{bmatrix} 78 | \begin{bmatrix} 79 | 3 & -2 \\ 1 & 1 80 | \end{bmatrix} 81 | \begin{bmatrix} 82 | x \\ y 83 | \end{bmatrix} 84 | = 85 | \begin{bmatrix} 86 | x & y 87 | \end{bmatrix} 88 | \begin{bmatrix} 89 | 3x - 2y \\ x + y 90 | \end{bmatrix} 91 | = 3x^2 - 2xy + xy + y^2 92 | \end{equation} 93 | 94 | 95 | Graphs 96 | ================== 97 | 98 | General definitions 99 | ------------------- 100 | 101 | Examples 102 | --------- 103 | 104 | **************** 105 | * * 106 | * * * 107 | * * 108 | **************** 109 | 110 | 111 | **************** 112 | * * 113 | * *----* * 114 | * * 115 | **************** 116 | 117 | 118 | 119 | ********************* 120 | * * 121 | * *----*----* * 122 | * * 123 | ********************* 124 | 125 | 126 | ********************* 127 | * * * 128 | * | * 129 | * | * 130 | * *----*----* * 131 | * * 132 | * * 133 | * * 134 | ********************* 135 | 136 | ************************ 137 | * * 138 | * * * * 139 | * \ / * 140 | * *----* * 141 | * / \ * 142 | * * * * 143 | * * 144 | ************************ 145 | 146 | 147 | 148 | 149 | Matrices associated with graphs 150 | ================================ 151 | 152 | 153 | Calculus with Laplacian matrices 154 | ================================= 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # `Sursis` - [personal] <- [notebook] -> [network] 2 | 3 | `Sursis` is a web app built on top of [Streamlit](https://streamlit.io) (somewhat abusively). 4 | 5 | Its basic purpose is to have a personal notebook or journal to jot down ideas, movies or records you have recently enjoyed or pieces of information related to personal projects. 6 | 7 | The particular conceit of `Sursis` is that this journal is not written sequentially, but as a network/graph. (It's been recently compared to mind-mapping, but without exception every such tool seems to expect you to figure out a tree-like outline. `Sursis` is much, much more general if that's the comparison to be made). 8 | 9 | 10 | Sursis runs locally. My personal suggestion is to set it up on a cheap VPS/Digital Ocean-type VM so you'll be able to use it on your phone. I've been having great fun with it from day zero and polishing it a little more each day. (The screenshots below are very out of date.) 11 | 12 | 13 | ## Input modes 14 | 15 | The app presents you with a number of choices. 16 | 17 | 18 | 19 | 20 | Let us look at them a little. 21 | 22 | 23 | 24 | Visualization lets you look at a node's immediate neighbors, in turn their neighbors and so on. We'll return to that. 25 | 26 | 27 | 28 | "Nodes" lets you add new nodes to your notebook. Shocking, no? 29 | 30 | 31 | 32 | "Connections" lets you add edges between nodes, i.e. connect them. 33 | 34 | 35 | 36 | Each prompt that requires you to select an already-existing node opens up a select-box. You can type to get partial results and confirm. 37 | 38 | 39 | With "Dyad" you can add two nodes at once and connect them. This can be done with the previous functions alone, but this is more convenient when writing stuff down quickly. 40 | 41 | 42 | Likewise "Triad" lets you add a "tree" or "fork"-like pattern with one new node that connects to other two nodes. This again is a convenience function. 43 | 44 | 45 | 46 | "Trailing" lets you add a new node and connect it to an existing node. 47 | 48 | 49 | 50 | "Merge" lets you merge two nodes and all their connections/edges. This is also useful to rename nodes. 51 | 52 | ## Visualization 53 | 54 | 55 | 56 | You can start from any node and look at its neighborhood, 57 | 58 | 59 | 60 | and then the neighborhood of its neighbors, 61 | 62 | 63 | 64 | and then the neighbors of their neighors of their neighbors. 65 | 66 | By scrolling down you can also see the minimum spanning tree, which cuts out some edges to provide a graph without cycles. 67 | 68 | 69 | 70 | For kicks, you can also see the full graph 71 | 72 | 73 | 74 | as well as its minimum spanning tree: 75 | 76 | 77 | 78 | 79 | ## The coloring of nodes 80 | 81 | The coloring of nodes is giving by a regularized least-squares solution to a Poisson equation 82 | 83 | Lx = w, 84 | 85 | where `w` are the observed "weights" (currently, the betweenness centrality of nodes). This, of course, is Gauss's gravity equation in natural units. In this way we expect to capture an idea of the balance of forces in the graph structures -- much beyond the 2D graph layouts that are really optimized for visualization, not insight. 86 | 87 | Edges are also given values in a very similar way, but using the dual graph (whose nodes are the edges of the primal graph). Layout algorithms are supposed to take these values into account by making higher-valued edges visually longer. I'm not so sure about that. 88 | 89 | ## Installing 90 | 91 | Clone the repository and create an environment 92 | 93 | python3 -m venv env 94 | source env/bin/activate 95 | pip install -r requirements.txt 96 | 97 | If you don't have a preexisting notebook, run 98 | 99 | python initialize_script.py 100 | 101 | Note that for the time being the notebook is always stored as `data.sqlite`. 102 | 103 | To run, 104 | 105 | streamlit run app.py 106 | 107 | You *will* get an error in the default visualization screen with an empty notebook/graph. Just add some nodes and then connections. 108 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | streamlit 2 | networkx 3 | matplotlib 4 | scipy 5 | graph-tools 6 | hy -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setuptools 2 | 3 | setup(name="sursis", 4 | version="0.1", 5 | description="Personal notebook network", 6 | url="https://github.com/asemic-horizon/sursis", 7 | author="asemic horizon", 8 | author_email="undisclosed", 9 | license="MIT", 10 | packages=["sursis"], 11 | zip_safe = False) 12 | 13 | -------------------------------------------------------------------------------- /sursis/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asemic-horizon/sursis/c7b8244d26f43eb67642146991622cbaf84046f4/sursis/__init__.py -------------------------------------------------------------------------------- /sursis/app.py: -------------------------------------------------------------------------------- 1 | import streamlit as st 2 | st.set_option('deprecation.showPyplotGlobalUse', False) 3 | st.set_page_config(layout='wide',page_title='casino rhizome',page_icon=":brain:") 4 | #st.beta_set_page_config(layout="wide") 5 | 6 | import networkx as nx 7 | # 8 | import backend.db as db 9 | import backend.physics as phys 10 | import backend.graph_physics as chem 11 | # 12 | import graph_views as gv 13 | import ui_elems as ui 14 | import dialogs as dlg 15 | # 16 | from backend.db import nc 17 | ### 18 | 19 | 20 | node_mode = "New node" 21 | edge_mode = "Connect" 22 | cluster_mode = "Cluster" 23 | trail_mode = "Trailing new" 24 | dyad_mode = "Dyad" 25 | triad_mode = "Triad" 26 | merge_mode = "Merge" 27 | view_mode = "Visualization" 28 | stats_mode = "Advanced" 29 | spath_mode = "Spath" 30 | 31 | with nc() as conn: 32 | # st.write("## `sursis`") 33 | col1, col2 = st.columns(2) 34 | 35 | major_mode = col1.radio(label="Major mode",\ 36 | options=["Browse","Edit"]) 37 | if major_mode == "Browse": 38 | op_mode=col2.radio(label="Operation mode",\ 39 | options=[view_mode, spath_mode, stats_mode]) 40 | elif major_mode == "Edit": 41 | op_mode = col2.radio(label="Operation mode",\ 42 | options=[trail_mode, dyad_mode, triad_mode,merge_mode, edge_mode, 43 | node_mode, 44 | ]) 45 | 46 | if op_mode == node_mode: 47 | ui.separator() 48 | st.write("### Add/remove nodes") 49 | dlg.node_entry(conn) 50 | elif op_mode == dyad_mode: 51 | ui.separator() 52 | st.write("### Add two nodes and connect them") 53 | dlg.dyad_entry(conn) 54 | elif op_mode == triad_mode: 55 | ui.separator() 56 | st.write("### Add a parent node and two children") 57 | dlg.triad_entry(conn) 58 | elif op_mode == edge_mode: 59 | ui.separator() 60 | st.write("### Add/remove connections") 61 | dlg.edge_entry(conn) 62 | elif op_mode== cluster_mode: 63 | ui.separator() 64 | st.write("### Cluster connect") 65 | dlg.cluster_connect(conn) 66 | elif op_mode == trail_mode: 67 | ui.separator() 68 | st.write("### Add new node and connect to existing") 69 | dlg.trail_node_entry(conn) 70 | elif op_mode == merge_mode: 71 | ui.separator() 72 | st.write("### Merge nodes") 73 | dlg.node_merge(conn) 74 | elif op_mode == spath_mode: 75 | dlg.spaths(conn) 76 | elif op_mode == stats_mode: 77 | dlg.advanced(conn) 78 | elif op_mode == view_mode: 79 | #ui.separator() 80 | c1, c2 = st.beta_columns(2) 81 | full_graph = c1.checkbox("Full graph",value=False) 82 | communities = True if full_graph else False #st.checkbox("Communities", value = False) 83 | 84 | if full_graph: 85 | center, radius = None, None 86 | if not full_graph: 87 | order = c2.checkbox("Order by energy", value=False) 88 | fields = list(reversed(db.list_nodes(conn, order = order))) 89 | u = 0 90 | d1,d2 = st.beta_columns(2) 91 | center = d1.selectbox("Choose nodes",fields,index = u) 92 | radius = d2.number_input("Radius",value=3) 93 | G = chem.graph(conn,center,radius) 94 | gv.graph_plot(G, conn,center,radius, communities) 95 | 96 | mintree = st.checkbox("Minimum tree", value = False) 97 | if mintree: gv.mintree(G,conn) 98 | 99 | #maxtree = st.checkbox("Maximum tree", value = False) 100 | #if maxtree: gv.maxtree(G,conn) 101 | 102 | energy_density = st.checkbox("Energy density", value = True) 103 | if energy_density: 104 | gv.view_energy(G,conn) 105 | #gv.view_gravity(G,conn) 106 | 107 | dd = st.checkbox("Degree distribution", value = True) 108 | if dd: gv.view_degrees(G,conn) 109 | 110 | spectrum = st.checkbox("Spectrum",value=False) 111 | if spectrum: gv.view_spectrum(G,conn) 112 | -------------------------------------------------------------------------------- /sursis/backend/data.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asemic-horizon/sursis/c7b8244d26f43eb67642146991622cbaf84046f4/sursis/backend/data.sqlite -------------------------------------------------------------------------------- /sursis/backend/db.py: -------------------------------------------------------------------------------- 1 | import logging 2 | logging.basicConfig(filename="app.log") 3 | 4 | import os, json, sqlite3 5 | # 6 | from numpy import unique, array 7 | from time import ctime 8 | import networkx as nx 9 | # 10 | import backend.physics as phys 11 | 12 | 13 | def is_numericstring(s : str) -> bool: 14 | return all([v in "0123456789." for v in s]) 15 | 16 | def surefloat(s): 17 | if isinstance(s,float) or isinstance(s,int): 18 | return s 19 | if isinstance(s,str) and is_numericstring(s): 20 | try: 21 | return float(s) 22 | except: 23 | return 0 24 | else: 25 | return 0 26 | 27 | def stringfloat(s): 28 | if isinstance(s,str) and is_numericstring(s): 29 | return float(s) 30 | else: 31 | return 0 32 | 33 | def dt(s): 34 | if len(s)==0: 35 | return None 36 | elif len(s)==1: 37 | return stringfloat(s[0]) 38 | else: 39 | return [stringfloat(u) for u in s] 40 | 41 | 42 | 43 | def nc(file="data.sqlite"): 44 | try: 45 | conn = sqlite3.connect(file) 46 | return conn 47 | except sqlite3.Error as e: 48 | logging.error(e) 49 | 50 | def run_sql(conn, sql, *args): 51 | cur = conn.cursor() 52 | cur.execute(sql,(*args,)) 53 | return cur 54 | 55 | def push(conn, sql, *args): 56 | return run_sql(conn,sql,*args).lastrowid 57 | 58 | def grab(conn, sql, *args): 59 | cur = run_sql(conn,sql,*args) 60 | res = cur.fetchone() 61 | return dt(res) 62 | 63 | def pull(conn, sql, *args): 64 | cur = run_sql(conn,sql,*args) 65 | res = cur.fetchall() 66 | return [dt(r) for r in res] 67 | 68 | 69 | # insert and delete functions do db work 70 | def insert_node(conn,node): 71 | push(conn, 72 | "INSERT INTO nodes (name) VALUES (?)",node) 73 | 74 | def node_exists(conn, node): 75 | res = pull(conn, 76 | "SELECT id FROM nodes WHERE name = ? LIMIT 1",node) 77 | return len(res)>0 78 | 79 | def count_nodes(conn): 80 | return grab(conn, 81 | "select (count (name)) from nodes;") 82 | 83 | 84 | # edges are written in one direction, but are assumed to be undirected 85 | # therefore must check both ways 86 | 87 | def edge_exists(conn, node_1, node_2): 88 | query = "SELECT id FROM named_edges WHERE node_1 = ? and node_2 = ? " 89 | res1 = run_sql(conn,query, node_1, node_2).fetchall() 90 | res2 = run_sql(conn,query, node_2, node_1).fetchall() 91 | return len(res1)>0 and len(res2)>0 92 | 93 | # This helper is necessary for inserts given as node names 94 | def get_node_id(conn,node): 95 | try: 96 | return run_sql(conn,"SELECT id FROM nodes WHERE name = ? LIMIT 1",node).fetchall()[0][0] 97 | except: 98 | logging.error("Error fetching id for node " + node) 99 | 100 | # edges are written in one direction, but are assumed to be undirected 101 | def insert_edge(conn, node_1, node_2): 102 | try: 103 | # ensure pattern for edges where n1 dominates alphabetically 104 | n1, n2 = min(node_1, node_2), max(node_1,node_2) 105 | id_1, id_2 = get_node_id(conn,n1),get_node_id(conn,n2) 106 | return run_sql(conn,"INSERT INTO edges (left,right) VALUES (?,?)",id_1,id_2).lastrowid 107 | except: 108 | logging.error(f"Couldn't create edge {node_1}-{node_2}") 109 | 110 | # edges are written in one direction, but are assumed to be undirected 111 | # therefore must check both ways 112 | 113 | def delete_edge(conn, node_1, node_2): 114 | query = "DELETE FROM edges WHERE left = ? and right = ?" 115 | try: 116 | id_1, id_2 = get_node_id(conn,node_1),get_node_id(conn,node_2) 117 | run_sql(conn,query,id_1,id_2) 118 | run_sql(conn,query,id_2,id_1) 119 | except: 120 | logging.error(f"Couldn't get nodes {node_1}-{node_2}") 121 | 122 | def delete_node(conn, node): 123 | try: 124 | id_1 = get_node_id(conn,node) 125 | # first delete connected edges. 126 | run_sql(conn,"DELETE FROM edges WHERE left = ? ",id_1) 127 | run_sql(conn,"DELETE FROM edges WHERE right = ?",id_1) 128 | run_sql(conn,"DELETE FROM nodes WHERE id = ?", id_1) 129 | except: 130 | logging.error(f"Couldn't {node}.") 131 | 132 | # write and del functions are called by the app an return a boolean 133 | def write_node(conn,node): 134 | node = node.lower() 135 | if node_exists(conn,node): 136 | return True 137 | else: 138 | insert_node(conn,node) 139 | def write_edge(conn,node_1,node_2): 140 | if edge_exists(conn,node_1,node_2): 141 | return True 142 | else: 143 | insert_edge(conn,node_1,node_2) 144 | return False 145 | def del_node(conn,node): 146 | if not node_exists(conn,node): 147 | return False 148 | else: 149 | delete_node(conn,node) 150 | return True 151 | def del_edge(conn,node_1,node_2): 152 | if edge_exists(conn,node_1,node_2): 153 | return True 154 | else: 155 | delete_edge(conn,node_1,node_2) 156 | return False 157 | 158 | # meant to be called directly from the app too. 159 | def list_nodes(conn, where=None, order = False): 160 | 161 | query = "SELECT name FROM nodes" 162 | if order: query+= " ORDER BY energy ASC" 163 | if where: query += " WHERE " + where 164 | return [n[0] for n in run_sql(conn,query).fetchall()] 165 | 166 | # not currently used, just for completeness 167 | def list_edges(conn): 168 | query = """SELECT * FROM named_edges""" 169 | return run_sql(conn,query).fetchall() 170 | 171 | def query_connections(conn,node): 172 | connected = run_sql(conn,"SELECT node_1, node_2 FROM named_edges WHERE node_1 = ?").fetchall() 173 | connected += run_sql(conn,"SELECT node_1, node_2 FROM named_edges WHERE node_2 = ?").fetchall() 174 | return connected 175 | 176 | def merge_nodes(conn,node1,node2,new_name = None): 177 | if new_name == None: 178 | new_name = f"{node1}/{node2}" 179 | write_node(conn,new_name) 180 | new_edges = query_connections(conn,node1)\ 181 | + query_connections(conn,node2) 182 | for u,v in new_edges: 183 | write_edge(conn,u,v) 184 | del_node(conn,node1); del_node(conn,node2) 185 | 186 | -------------------------------------------------------------------------------- /sursis/backend/db_init.py: -------------------------------------------------------------------------------- 1 | from backend import db 2 | from networkx import line_graph as dual 3 | import sqlite3, logging 4 | 5 | 6 | def initialize(conn): 7 | sql_nodes_table =\ 8 | """CREATE TABLE IF NOT EXISTS nodes ( 9 | id integer PRIMARY KEY, 10 | name text NOT NULL, 11 | mass real, 12 | energy real, 13 | degree real);""" 14 | sql_edges_table =\ 15 | """CREATE TABLE IF NOT EXISTS edges ( 16 | id integer PRIMARY KEY, 17 | left integer NOT NULL, 18 | right integer NOT NULL, 19 | mass real, 20 | energy real, 21 | degree real);""" 22 | sql_named_edges_view=\ 23 | """CREATE VIEW named_edges as 24 | SELECT edges.id, 25 | n1.name as node_1, 26 | n2.name as node_2, 27 | edges.mass, 28 | edges.energy, 29 | edges.degree 30 | FROM edges 31 | LEFT JOIN nodes AS n1 32 | ON n1.id = edges.left 33 | LEFT JOIN nodes AS n2 34 | ON n2.id = edges.right;""" 35 | try: 36 | c = conn.cursor() 37 | c.execute(sql_nodes_table) 38 | c.execute(sql_edges_table) 39 | c.execute(sql_named_edges_view) 40 | except sqlite3.Error as e: 41 | logging.error(e) 42 | 43 | 44 | 45 | 46 | def reset(conn, G): 47 | initialize(conn) 48 | # NODES 49 | for node, data in G.nodes(data=True): 50 | db.push(conn,\ 51 | """INSERT INTO nodes (name, mass, energy, degree) 52 | VALUES (?, ?, ?, ?)""", 53 | node, data["mass"], data["energy"], G.degree[node]) 54 | # EDGES 55 | H = dual(G) 56 | for edge, data in H.nodes(data = True): 57 | n1 = min(edge); n2 = max(edge) 58 | id_1, id_2 = db.get_node_id(conn,n1), db.get_node_id(conn,n2) 59 | db.push(conn,\ 60 | """INSERT INTO edges (left, right, mass, energy, degree) 61 | VALUES (?, ?, ?, ?, ?)""", 62 | id_1, id_2, data["mass"], data["energy"], H.degree[edge]) 63 | -------------------------------------------------------------------------------- /sursis/backend/graph_physics.py: -------------------------------------------------------------------------------- 1 | import logging 2 | logging.basicConfig(filename="physics.log",level = logging.INFO) 3 | 4 | from backend import db 5 | from backend import physics as phys 6 | # 7 | from networkx import line_graph as dual 8 | import networkx as nx 9 | import numpy as np 10 | from scipy.stats import norm 11 | from numpy import array 12 | 13 | def get_physics(conn, index, table): 14 | query = f"SELECT {index}, energy, mass, degree FROM {table}" 15 | return np.array(db.run_sql(conn,query).fetchall()) 16 | 17 | 18 | def graph(conn, center = None, radius = None, prop = "energy"): 19 | nodes = get_physics(conn, index = "name", table = "nodes") 20 | edges = get_physics(conn, index = "node_1, node_2", table = "named_edges") 21 | G = nx.Graph() 22 | for node, energy, mass, degree in nodes: 23 | G.add_node(node, 24 | weight=db.surefloat(energy), 25 | energy=db.surefloat(energy), 26 | mass = db.surefloat(mass), 27 | degree=int(db.surefloat(degree))) 28 | for u,v, energy, mass, degree in edges: 29 | if u in G.nodes() and v in G.nodes(): 30 | if degree and db.surefloat(degree)>0: 31 | w = db.surefloat(degree)/(4+db.surefloat(energy)) 32 | # w = norm.cdf(0.1 - db.surefloat(energy)*db.surefloat(mass)) 33 | else: 34 | w = 0.5 35 | G.add_edge(u,v,weight=w if w>0 else 0, 36 | energy = db.surefloat(energy), 37 | mass = db.surefloat(mass), 38 | degree = int(db.surefloat(degree))) 39 | if center and radius: 40 | G = nx.ego_graph(G,n=center, radius=radius) 41 | return G 42 | 43 | def digraph(conn, center = None, radius = None, prop = "energy"): 44 | nodes = get_physics(conn, index = "name", table = "nodes") 45 | edges = get_physics(conn, index = "node_1, node_2", table = "named_edges") 46 | G = nx.DiGraph() 47 | for node, energy, mass, degree in nodes: 48 | G.add_node(node, 49 | weight=db.surefloat(energy), 50 | energy=db.surefloat(energy), 51 | mass = db.surefloat(mass), 52 | degree=int(db.surefloat(degree))) 53 | for u,v, energy, mass, degree in edges: 54 | if u in G.nodes() and v in G.nodes(): 55 | w = 1/norm.cdf(0.1 - db.surefloat(energy)*db.surefloat(mass)) 56 | if db.surefloat(energy) >= 0: 57 | source, sink = u, v 58 | else: 59 | source, sink = v, u 60 | G.add_edge(source,sink,weight=w if w>0 else 0, 61 | energy = db.surefloat(energy), 62 | mass = db.surefloat(mass), 63 | degree = int(db.surefloat(degree))) 64 | if center and radius: 65 | G = nx.ego_graph(G,n=center, radius=radius) 66 | return G 67 | 68 | 69 | def update_physics(conn,model="bvp", nb = None, eb = None, fast = True): 70 | G = graph(conn, center = None) 71 | logging.info("Calculate node physics") 72 | # NODES 73 | mass, energy = phys.physics(graph=G, model=model, boundary_value = nb, bracket=(-np.inf,np.inf),fast=fast) 74 | for node, mass, energy in zip(G.nodes(), mass, energy): 75 | db.push(conn,\ 76 | """UPDATE nodes SET mass = ?, energy = ?, degree = ? 77 | WHERE name = ? """, 78 | mass, energy, G.degree[node],node) 79 | 80 | # EDGES 81 | logging.info("Calculate edge physics") 82 | H = dual(G); del G 83 | mass, energy = phys.physics(graph=H, model=model, boundary_value=eb,bracket=(-np.inf,np.inf),fast=fast) 84 | values = [(u,v,m,p) for (u,v),m, p in zip(H.nodes(),mass,energy)] 85 | 86 | for u, v, mass, energy in values: 87 | n1 = min(u,v); n2 = max(u,v) 88 | id_1, id_2 = db.get_node_id(conn,n1), db.get_node_id(conn,n2) 89 | db.push(conn,\ 90 | """UPDATE edges SET mass = ?, energy = ?, degree = ? 91 | WHERE (left = ? and right = ?) 92 | OR (right = ? and left = ?) """, 93 | mass, energy, H.degree[(u,v)], id_1, id_2, id_1, id_2) 94 | 95 | 96 | def read_node_prop(conn,subgraph,prop="energy"): 97 | nodes = list(subgraph.nodes()) 98 | res = [db.run_sql(conn,\ 99 | f"SELECT {prop} FROM nodes WHERE name = ?", n).fetchall()[0][0] for n in nodes] 100 | return array([db.surefloat(r) for r in res]) 101 | 102 | def read_edge_prop(conn,subgraph,prop="energy"): 103 | edges = list(subgraph.edges()) 104 | res = [db.run_sql(conn,\ 105 | "SELECT energy FROM edges WHERE left = ? AND right = ?", u,v).fetchall()[0][0] for u,v in edges] 106 | return array([get_edge_energy(conn,u,v) for u,v in edges]) 107 | 108 | def prop_bounds(conn,prop="energy",table="nodes",slices=4): 109 | count = db.run_sql(conn,f"SELECT (COUNT(*)) FROM {table}").fetchone()[0] 110 | 111 | min_val = db.run_sql(conn,f"select MIN({prop}) FROM {table}").fetchone()[0] 112 | 113 | avg_val = db.run_sql(conn,f"select AVG({prop}) FROM {table}").fetchone()[0] 114 | med_val = db.run_sql(conn,\ 115 | f"""SELECT {prop} FROM {table} ORDER BY {prop} LIMIT 1 116 | OFFSET 117 | {count//2} 118 | 119 | """).fetchone()[0] 120 | max_val = db.run_sql(conn,f"select MAX({prop}) FROM {table}").fetchone()[0] 121 | 122 | 123 | return min_val, max_val, avg_val, med_val 124 | 125 | 126 | def total_energy(conn, table = "nodes",where=""): 127 | return db.run_sql(conn,\ 128 | f"SELECT SUM((mass * energy)) from {table} {where}").fetchone()[0] 129 | 130 | def subgraph_energy(conn,subgraph): 131 | m = read_node_prop(conn,subgraph,"mass") 132 | e = read_node_prop(conn,subgraph,"energy") 133 | return np.sum(m*e) 134 | 135 | def gravity_partition(G, conn): 136 | expanding = db.list_nodes(conn, "(mass * energy) >0") 137 | collapsing = db.list_nodes(conn, "(mass * energy) < 0") 138 | return G.subgraph(expanding), G.subgraph(collapsing) 139 | 140 | def boundary(conn,table="nodes", deg = 1): 141 | return db.run_sql(conn, 142 | f"SELECT AVG(energy) FROM {table} WHERE degree={deg}").fetchone()[0] 143 | 144 | 145 | #end 146 | -------------------------------------------------------------------------------- /sursis/backend/physics.py: -------------------------------------------------------------------------------- 1 | import logging 2 | logging.basicConfig(filename="physics.log",level = logging.INFO) 3 | 4 | import networkx as nx 5 | import scipy 6 | import numpy as np 7 | 8 | 9 | def physics(graph : nx.Graph, 10 | boundary_value = 0.025, model = None,\ 11 | bracket=(-np.inf,np.inf), crit_degree=2, fast = True ): 12 | m = mass(graph) 13 | if model == "penrose": 14 | return m, least_squares_potential(graph,m) 15 | elif model == "dirichlet": 16 | return m, dirichlet_potential(graph,m, boundary_value,\ 17 | crit_degree) 18 | else: 19 | return m, constrained_potential(graph,m, boundary_value,\ 20 | crit_degree,bracket, fast) 21 | 22 | def boundary(graph,crit_degree): 23 | return [n\ 24 | for n,f in enumerate(graph.nodes)\ 25 | if graph.degree[f]==crit_degree] 26 | 27 | def internal(graph,crit_degree): 28 | return [n\ 29 | for n,f in enumerate(graph.nodes)\ 30 | if graph.degree[f]!=crit_degree] 31 | 32 | def least_squares_boundary(graph, value = 0.0, crit_degree=1,\ 33 | lower = -np.inf, higher = np.inf,\ 34 | eps = 1e-10): 35 | n = graph.number_of_nodes() 36 | lb = np.full((n,),lower); ub = np.full((n,),higher) 37 | gb = boundary(graph,crit_degree) 38 | lb[gb] = value-eps 39 | ub[gb] = value+eps 40 | return (lb,ub) 41 | 42 | def mass(graph): 43 | m1 = nx.betweenness_centrality(graph) 44 | m1 = np.array(list(dict(m1).values())) 45 | return m1/np.sum(m1) 46 | 47 | def penrose_potential(graph : nx.Graph,mass : np.ndarray): 48 | rho = mass.reshape(1,-1) 49 | L = nx.laplacian_matrix(graph) 50 | L_inv = scipy.linalg.pinv(L.todense()) 51 | return L_inv.dot(mass) 52 | 53 | def least_squares_potential(graph: nx.Graph, mass : np.ndarray): 54 | rho = mass.reshape(-1,1) 55 | L = nx.laplacian_matrix(graph) 56 | sol = scipy.sparse.linalg.lsmr(L, -rho,damp=1e-3) 57 | return sol[0] 58 | 59 | def constrained_potential(graph: nx.Graph, 60 | mass: np.ndarray, 61 | boundary_value, 62 | crit_degree, 63 | bracket, 64 | fast = True): 65 | rho = mass.reshape(-1,) 66 | L = nx.laplacian_matrix(graph) 67 | bounds = least_squares_boundary(graph, boundary_value,\ 68 | crit_degree, *bracket) 69 | logging.info(f"Fast recalc: {fast}") 70 | sol = scipy.optimize.lsq_linear( 71 | L, 72 | -rho, 73 | bounds=bounds, 74 | max_iter = 10 if fast else 5000) 75 | logging.info("Optimality: " + sol.message) 76 | return sol.x 77 | 78 | def dirichlet_potential(graph: nx.Graph, 79 | mass: np.ndarray, 80 | boundary_value, 81 | crit_degree, 82 | max_iter = 40, 83 | tol = 1e-7): 84 | boundary_nodes = boundary(graph, crit_degree) 85 | internal_nodes = internal(graph, crit_degree) 86 | deg = list(dict(graph.degree()).values()) 87 | 88 | A = nx.adjacency_matrix(graph) 89 | print(np.linalg.eigvals(A.todense())) 90 | dominant = 1.02*max(np.absolute(np.linalg.eigvals(A.todense()))) 91 | dominant = dominant/max(deg) 92 | print(f"Scale by {dominant:3.2f}") 93 | A = (1/dominant)*A 94 | phi = -0.001*np.ones(shape=(graph.number_of_nodes(),)) 95 | 96 | phi[boundary_nodes] = boundary_value 97 | 98 | # A fixed point of x[n+1]=C*x[n]+b is such that 99 | # x = Cx+b => (I-C) x = b =>x = inv(I-C)*b 100 | # Hence this iteration can be used to invert I-C. 101 | # 102 | # Now, Ly = (D-A)y = D(I-inv(D)*A) = mass 103 | # Hence the iteration x(n+1)=inv(D)*A*x(n) +mass 104 | # 105 | # But only on internal nodes. 106 | 107 | err = np.inf; iter = 0 108 | while err>tol or iter < max_iter: 109 | phi_hat = phi.copy() 110 | for row in internal_nodes: 111 | z = np.dot(A[row,:].todense(),phi) 112 | if deg[row]>0: 113 | phi_hat[row] = z.reshape(-1,)[0]/deg[row] - mass[row] 114 | 115 | err = scipy.linalg.norm(phi - phi_hat) 116 | print(iter,err) 117 | phi = phi_hat.copy() 118 | iter += 1 119 | logging.info(f"After {iter} steps, estimate change of {err:.4e}") 120 | return dominant*phi 121 | -------------------------------------------------------------------------------- /sursis/backend/stats.py: -------------------------------------------------------------------------------- 1 | import networkx as nx 2 | import collections 3 | import numpy as np 4 | import scipy 5 | # 6 | from backend import physics as phys 7 | from backend import graph_physics as chem 8 | from scipy.optimize import curve_fit 9 | 10 | def degree_distribution(graph): 11 | degree_sequence = sorted([d for n, d in graph.degree()], reverse=True) # degree sequence 12 | degreeCount = collections.Counter(degree_sequence) 13 | deg, cnt = zip(*degreeCount.items()) 14 | return deg, cnt 15 | 16 | def spectrum(graph): 17 | L = nx.laplacian_matrix(graph) 18 | eigvals = np.linalg.eigvals(L.toarray()) 19 | return eigvals 20 | 21 | def power_law(x,k,slope): 22 | x = np.array(x) 23 | return k*(x**slope) 24 | 25 | def fit_power_distribution(deg,cnt): 26 | popt, _ = curve_fit(f=power_law,xdata=deg[:-1],ydata=cnt[:-1]) 27 | k, slope = tuple(popt) 28 | return k, slope 29 | 30 | def leaf_analysis(graph): 31 | deg, cnt = degree_distribution(graph) 32 | leaves = filter(lambda x: graph.degree(x)==1, graph.nodes()) 33 | leaves = list(leaves) 34 | print(leaves) 35 | k, slope = fit_power_distribution(deg,cnt) 36 | return len(leaves),k,slope 37 | 38 | -------------------------------------------------------------------------------- /sursis/data.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asemic-horizon/sursis/c7b8244d26f43eb67642146991622cbaf84046f4/sursis/data.sqlite -------------------------------------------------------------------------------- /sursis/dialogs.py: -------------------------------------------------------------------------------- 1 | import logging 2 | logging.basicConfig(filename="physics.log",level = logging.INFO) 3 | 4 | 5 | import streamlit as st 6 | from itertools import combinations 7 | from numpy.random import randint 8 | 9 | import ui_elems as ui 10 | import graph_views as gv 11 | from backend import db 12 | from backend import graph_physics as chem 13 | 14 | def node_entry(conn): 15 | node = st.text_input('Enter new node name') 16 | add_button, del_button = ui.add_del() 17 | if node and add_button: 18 | found = db.write_node(conn,node) 19 | conn.commit() 20 | ui.if_confirm(found,"Name already exists") 21 | if node and del_button: 22 | found = db.del_node(conn,node) 23 | conn.commit() 24 | ui.if_confirm(found) 25 | return None 26 | 27 | def spaths(conn): 28 | G = chem.graph(conn) 29 | node_1 = ui.known_field_input(conn,"Source",offset=0) 30 | node_2 = ui.known_field_input(conn,"Target",offset=10) 31 | st.write("### Shortest paths") 32 | gv.spaths(G,conn,node_1,node_2) 33 | 34 | def advanced(conn): 35 | dir_mode = "Dirichlet" 36 | bvp_mode = "Constrained least squares" 37 | penrose_mode = "Penrose" 38 | mode = st.radio("Physics mode",[bvp_mode, dir_mode, penrose_mode]) 39 | if mode == bvp_mode: 40 | nb = chem.boundary(conn,"nodes",deg=2) 41 | eb = chem.boundary(conn,"edges",deg=2) 42 | nb = st.number_input("Node boundary values",step=0.001,format="%2.4e") 43 | eb = st.number_input("Edge boundary values",step=0.001,format="%2.4e") 44 | but = st.button("Recalculate physics") 45 | if but: 46 | chem.update_physics(conn,model = "bvp",nb = nb, eb = eb, fast=False) 47 | st.write(f"System energy: {chem.total_energy(conn):2.3f}") 48 | ui.confirm() 49 | elif mode == dir_mode: 50 | nb = chem.boundary(conn,"nodes",deg=2) 51 | eb = chem.boundary(conn,"edges",deg=2) 52 | nb = st.number_input("Node boundary values",step=5e-3,format="%2.4e") 53 | eb = st.number_input("Edge boundary values",step=0.0,format="%2.4e") 54 | but = st.button("Recalculate physics") 55 | if but: 56 | chem.update_physics(conn,model = "dirichlet",nb = nb, eb = eb, fast=False) 57 | st.write(f"System energy: {chem.total_energy(conn):2.3f}") 58 | ui.confirm() 59 | 60 | elif mode == penrose_mode: 61 | but = st.button("Recalculate physics") 62 | if but: 63 | chem.update_physics(conn,model = "penrose",nb = 0, eb = 0, fast=False) 64 | 65 | 66 | G = chem.graph(conn) 67 | gv.view_energy(G,conn) 68 | gv.view_degrees(G,conn) 69 | gv.view_spectrum(G,conn) 70 | 71 | 72 | def edge_entry(conn): 73 | node_1 = ui.known_field_input(conn,"Source",offset=0) 74 | node_2 = ui.known_field_input(conn,"Target",offset=1) 75 | add_button, del_button = ui.add_del() 76 | if node_1 and node_2 and add_button: 77 | db.write_edge(conn, node_1,node_2) 78 | conn.commit(); ui.confirm() 79 | if node_1 and node_2 and del_button: 80 | found = True 81 | while found: 82 | found = db.del_edge(conn,node_1, node_2) 83 | st.write("(Edge deleted.)") 84 | conn.commit(); ui.confirm() 85 | return None 86 | 87 | def cluster_connect(conn): 88 | n_units = st.number_input("Number of nodes",value=2) 89 | units = dict() 90 | for i in range(n_units): 91 | units[i] = ui.known_field_input(conn,f"Source {i+1}:",offset=i) 92 | add = st.button("Connect") 93 | if add: 94 | for node_1, node_2 in combinations(units.values(),2): 95 | db.write_edge(conn, node_1, node_2) 96 | st.write(f"Added: {node_1}:{node_2}") 97 | conn.commit(); ui.confirm() 98 | 99 | def node_merge(conn): 100 | nodes = db.list_nodes(conn) 101 | u,v = 1,3 102 | node_1 = ui.known_field_input(conn,"Source 1",offset=0) 103 | node_2 = ui.known_field_input(conn,"Source 2",offset=1) 104 | if node_1 in nodes and node_2 in nodes: 105 | if node_1 == node_2: 106 | defval = node_1 107 | else: 108 | defval = f"{node_1}/{node_2}" 109 | new_node = st.text_input("New node", value=defval) 110 | merge_button = st.button("Merge") 111 | if merge_button: 112 | db.merge_nodes(conn,node_1,node_2, new_node) 113 | conn.commit(); ui.confirm() 114 | return None 115 | 116 | def trail_node_entry(conn): 117 | nodes = db.list_nodes(conn) 118 | node_1 = ui.known_field_input(conn,"Existing",offset=0) 119 | node_2 = st.text_input('New') 120 | 121 | nonn_button = st.button("Add and connect") 122 | if node_1 and node_2 and nonn_button: 123 | db.write_node(conn,node_2) 124 | db.write_edge(conn,node_1,node_2) 125 | conn.commit(); ui.confirm() 126 | return None 127 | 128 | def dyad_entry(conn): 129 | node_1 = st.text_input('Enter new node 1 name') 130 | node_2 = st.text_input('Enter new node 2 name') 131 | add_button, del_button = ui.add_del() 132 | if node_1 and node_2 and add_button: 133 | db.write_node(conn,node_1) 134 | db.write_node(conn,node_2) 135 | db.write_edge(conn,node_1, node_2) 136 | conn.commit(); ui.confirm() 137 | if node_1 and node_2 and del_button: 138 | db.del_edge(conn,node_1,node_2) 139 | db.del_node(conn,node_1) 140 | db.del_node(conn,node_2) 141 | conn.commit(); ui.confirm() 142 | return None 143 | 144 | def triad_entry(conn): 145 | parent = st.text_input('Enter head node name') 146 | left = st.text_input('Enter left child name') 147 | right = st.text_input('Enter right child name') 148 | add_button, del_button = ui.add_del() 149 | if parent and left and right and add_button: 150 | db.write_node(conn,parent) 151 | db.write_node(conn,left) 152 | db.write_node(conn,right) 153 | db.write_edge(conn,parent,left) 154 | db.write_edge(conn,parent,right) 155 | conn.commit(); ui.confirm() 156 | if parent and left and right and del_button: 157 | db.del_node(conn, parent) 158 | db.del_node(conn, left) 159 | db.del_node(conn, right) 160 | db.del_edge(conn, parent,left) 161 | db.del_edge(conn, parent,right) 162 | conn.commit(); ui.confirm() 163 | return None 164 | -------------------------------------------------------------------------------- /sursis/graph_views.py: -------------------------------------------------------------------------------- 1 | import streamlit as st 2 | import matplotlib.pyplot as plt 3 | import matplotlib.colors as colors 4 | import networkx as nx 5 | import collections 6 | import numpy as np 7 | from backend import physics as phys 8 | from backend import graph_physics as chem 9 | from backend import stats 10 | from scipy.stats import gaussian_kde 11 | import ui_elems as ui 12 | import viz 13 | 14 | #cmap = "RdYlBu" 15 | #cmap = "PuOr" 16 | cmap = "jet" 17 | #cmap = "coolwarm" 18 | #cmap = "bwr" 19 | #cmap = "gist_stern" 20 | #cmap = "PiYG_r" 21 | #cmap = "Spectral" 22 | #cmap = "nipy_spectral_r" 23 | #cmap = "terrain_r" 24 | 25 | def plot_degree_distribution(graph): 26 | deg, cnt = stats.degree_distribution(graph) 27 | deg = np.array(deg); cnt = np.array(cnt) 28 | k, slope = stats.fit_power_distribution(deg,cnt) 29 | plt.scatter(deg,cnt) 30 | plt.plot(deg,stats.power_law(deg,k,slope), linewidth=1,c='k',linestyle='dotted') 31 | plt.grid(True) 32 | plt.title("Degree distribution") 33 | 34 | st.pyplot() 35 | st.write(\ 36 | f"* Approximation: $f \\approx {k:2.0f}x^"+"{"+f"{slope:2.2f}"+"}$") 37 | 38 | def eigenvalues(graph): 39 | eigvals = stats.spectrum(graph) 40 | eigvals = eigvals[eigvals>1e-6] 41 | eigvals.sort() 42 | plt.scatter(np.arange(len(eigvals)),1/eigvals, alpha = 0.5) 43 | plt.yscale("log") 44 | plt.grid(True) 45 | plt.title("Inverse Laplacian eigenvalues") 46 | st.pyplot() 47 | st.write(f"* Spectral gap: {1/eigvals[0]-1/eigvals[1]:2.2f}") 48 | 49 | def mass(graph,conn): 50 | m = chem.read_node_prop(conn,graph,"mass") 51 | density = gaussian_kde(m) 52 | plt.plot(m,density(m)) 53 | plt.grid(True) 54 | plt.title("Mass") 55 | plt.xscale("log") 56 | st.pyplot() 57 | st.write(f"* Mean mass {np.mean(m):e}") 58 | 59 | def energy(graph,conn): 60 | e = chem.read_node_prop(conn,graph,"energy") 61 | grav = np.array(sorted(np.array(e)/len(e))) 62 | qtile = np.linspace(0,1,len(grav)) 63 | #density = gaussian_kde(grav) 64 | plt.plot(grav,qtile) 65 | plt.axhline(0.5,c='g',linestyle="dotted") 66 | plt.axvline(np.median(grav),c='g',linestyle="dotted") 67 | plt.axvline(np.mean(grav),c='r',linestyle="dashed") 68 | plt.axvline(0,c='y',linestyle="dotted") 69 | 70 | # plt.axhline(np.where(grav==np.mean(grav))[0],c="r",linestyle="dashed") 71 | plt.title("Cumulative energy") 72 | # grav = np.array(sorted(np.array(e)*np.array(m))) 73 | # density = gaussian_kde(grav) 74 | # plt.plot(grav,density(grav)) 75 | # plt.title("Gravity momentum") 76 | plt.grid(True); 77 | st.pyplot() 78 | st.write(f"* % expanding {100*len(grav[grav>1e-6])/len(grav):2.1f}%") 79 | 80 | 81 | 82 | def stats_view(graph): 83 | 84 | plot_degree_distribution(graph) 85 | eigenvalues(graph) 86 | mass(graph) 87 | energy(graph) 88 | phase(graph) 89 | 90 | def sufficient(graph): 91 | return graph.number_of_nodes() > 5 92 | 93 | def graph_plot(G, conn, center, radius, communities = False): 94 | full_graph = center is None 95 | if full_graph: pos = nx.kamada_kawai_layout 96 | # a = -chem.subgraph_energy(conn,G) 97 | # b = -chem.total_energy(conn) 98 | # st.write(f"Net gravity = **{a:2.3f}** - {b:2.3f} = {a-b:2.3f}") 99 | viz.draw(G, conn, labels = not full_graph, cmap = cmap) 100 | try: 101 | leaves, expected_leaves, slope = stats.leaf_analysis(G) 102 | print(leaves) 103 | st.write(f"Exponent {slope:1.2f} predicts {expected_leaves:1.0f} terminal nodes, {leaves} found") 104 | except: 105 | pass 106 | #viz.draw(G,conn,labels = not full_graph, cmap=cmap) 107 | try: 108 | out, coll = chem.gravity_partition(G,conn) 109 | ui.separator() 110 | st.write("### Expanding") 111 | viz.draw(out,conn,cmap=cmap) 112 | st.write("### Collapsing") 113 | viz.draw(coll,conn,cmap=cmap) 114 | except: 115 | st.write("Couldn't make expanding/collapsing subsets") 116 | 117 | if full_graph: 118 | ui.separator() 119 | st.write("### Components") 120 | S = [G.subgraph(c).copy() for c in nx.connected_components(G)] 121 | for subgraph in S: 122 | viz.draw(subgraph, conn, cmap = cmap) 123 | ui.separator() 124 | 125 | if sufficient(G) and communities: 126 | u = nx.algorithms.community.kernighan_lin.kernighan_lin_bisection(G) 127 | thresh = 4 if full_graph else 4 128 | S = [G.subgraph(c).copy() for c in u if len(c)>thresh] 129 | st.write("### Communities") 130 | for subgraph in S: 131 | viz.draw(subgraph, conn, cmap = cmap) 132 | ui.separator() 133 | 134 | def spaths(G,conn, source, target): 135 | ps = nx.algorithms.shortest_paths.\ 136 | generic.all_shortest_paths(G,source,target) 137 | 138 | spath = nx.Graph() 139 | for q in ps: 140 | for p in q: 141 | S = nx.ego_graph(G,n=p,radius = 1) 142 | spath = nx.compose(spath,S) 143 | st.write(f"Steps: {len(q)-1}") 144 | st.write(f" $\\to$ ".join(q)) 145 | viz.draw(spath,conn,cmap=cmap) 146 | 147 | def lpaths(G, conn, source, target): 148 | cutoff = st.number_input("Max lookup", value=8) 149 | pls = nx.all_simple_paths(G, source, target,cutoff=cutoff) 150 | if not pls: st.write(list(pls)) 151 | max_len = max([len(p) for p in pls]) 152 | pl = [p for p in pls if len(p)==max_len] 153 | lpath = nx.Graph() 154 | for q in pl: 155 | for p in q: 156 | L = nx.ego_graph(G,n=p,radius = 1) 157 | lpath = nx.compose(lpath,L) 158 | st.write(f"Steps: {len(q)-1}") 159 | st.write(f" $\\to$ ".join(q)) 160 | viz.draw(lpath,conn,cmap=cmap) 161 | 162 | 163 | def mintree(G,conn): 164 | 165 | if sufficient(G): 166 | H = nx.minimum_spanning_tree(G) 167 | ui.separator() 168 | st.write("#### Minimum tree") 169 | viz.draw(H, conn, cmap = cmap) 170 | st.pyplot() 171 | 172 | def maxtree(G,conn): 173 | 174 | if sufficient(G): 175 | J = nx.maximum_spanning_tree(G) 176 | st.write("#### Maximum tree") 177 | viz.draw(J, conn, cmap = cmap) 178 | st.pyplot() 179 | 180 | def view_energy(G,conn): 181 | if sufficient(G): 182 | ui.separator() 183 | st.write("### Energy density") 184 | energy(G,conn) 185 | 186 | # def view_gravity(G,conn): 187 | # if sufficient(G): 188 | # ui.separator() 189 | # st.write("### Momentum") 190 | # gravity(G,conn) 191 | 192 | 193 | def view_spectrum(G,conn): 194 | if sufficient(G): 195 | ui.separator() 196 | st.write("### Laplacian spectrum") 197 | eigenvalues(G) 198 | 199 | 200 | def view_degrees(G,conn): 201 | if sufficient(G): 202 | ui.separator() 203 | st.write("### Degree distribution") 204 | plot_degree_distribution(G) 205 | 206 | 207 | -------------------------------------------------------------------------------- /sursis/initialize_script.py: -------------------------------------------------------------------------------- 1 | from backend import db 2 | 3 | with db.nc() as conn: 4 | db.initialize() -------------------------------------------------------------------------------- /sursis/ui_elems.py: -------------------------------------------------------------------------------- 1 | import streamlit as st 2 | import networkx as nx 3 | import numpy as np 4 | from time import ctime 5 | from numpy.random import choice 6 | from backend import db 7 | 8 | def separator(): 9 | st.write("----") 10 | 11 | def confirm(): 12 | st.write(f"Operation confirmed at {ctime()}.") 13 | 14 | def if_confirm(pred,err="(Not found)"): 15 | if pred: 16 | confirm() 17 | else: 18 | st.write(err) 19 | 20 | def known_field_input(conn,tag="Node", default=None, offset = None): 21 | nodes = list(reversed(db.list_nodes(conn))) 22 | if default: 23 | index = nodes.index(default) 24 | elif offset: 25 | index = 2 26 | else: 27 | index = 1 28 | field_input = st.selectbox(tag,nodes,index=index) 29 | return field_input 30 | 31 | def add_del(tag1="Add",tag2="Delete"): 32 | add = st.button(tag1) 33 | rem = st.button(tag2) 34 | return add, rem 35 | 36 | 37 | def similar(node,substring_length = 7, max_examples = 2): 38 | effective_length = min(len(node),substring_length) 39 | nodes = db.list_nodes() 40 | candidates = [n for n in nodes if n[:effective_length]==node[:effective_length] and n!=node] 41 | effective_num_candidates = min(len(candidates),max_examples) 42 | return list(set(np.random.choice(candidates,effective_num_candidates))) 43 | 44 | # not currently used 45 | def graph_views(G): 46 | messages = [ 47 | f"{nx.number_of_nodes(G)} nodes, {nx.number_of_edges(G)} edges, {100*nx.density(G):2.2f}% density", 48 | f"{100*nx.average_clustering(G):2.2f}% of triangles, {100*nx.algorithms.cluster.transitivity(G):2.2f}% of triads", 49 | f"{nx.number_connected_components(G)} components", 50 | ] 51 | for msg in messages: 52 | st.write(f"* {msg}") 53 | 54 | -------------------------------------------------------------------------------- /sursis/viz.py: -------------------------------------------------------------------------------- 1 | import logging 2 | logging.basicConfig(filename="physics.log",level = logging.INFO) 3 | 4 | 5 | 6 | import streamlit as st 7 | import matplotlib.cm as cm 8 | import matplotlib as mpl 9 | import matplotlib.colors as colors 10 | import networkx as nx 11 | import numpy as np 12 | from backend import graph_physics as chem 13 | 14 | 15 | 16 | # set the colormap and centre the colorbar 17 | class MidpointNormalize(colors.Normalize): 18 | """ 19 | Normalise the colorbar so that diverging bars work there way either side from a prescribed midpoint value) 20 | 21 | e.g. im=ax1.imshow(array, norm=MidpointNormalize(midpoint=0.,vmin=-100, vmax=100)) 22 | """ 23 | def __init__(self, vmin=None, vmax=None, midpoint=None, clip=False): 24 | self.midpoint = midpoint 25 | colors.Normalize.__init__(self, vmin, vmax, clip) 26 | 27 | def __call__(self, value, clip=None): 28 | # I'm ignoring masked values and all kinds of edge cases to make a 29 | # simple example... 30 | x, y = [self.vmin, self.midpoint, self.vmax], [0, 0.5, 1] 31 | return np.ma.masked_array(np.interp(value, x, y), np.isnan(value)) 32 | 33 | 34 | def draw_bw(G): 35 | pos = nx.spectral_layout(G) 36 | pos = nx.kamada_kawai_layout(G,pos=pos,weight="mass") 37 | font_size = 10 if G.number_of_nodes()<10 else 6 38 | alpha = 1 if G.number_of_nodes()<150 else 0.855 39 | nx.draw(G,pos=pos,with_labels=True, node_color='w',font_size=font_size,width=0.2,alpha=alpha) 40 | st.pyplot() 41 | 42 | def draw_color(G, pot, window, labels, node_size = 50, cmap="gnuplot"): 43 | pos = nx.kamada_kawai_layout(G,weight="weight") 44 | N = G.number_of_nodes() 45 | font_size = 11 if N<10 else 9 46 | if N<10: 47 | alpha = 1 48 | elif 10