├── .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