├── .gitignore ├── LICENSE ├── README.md ├── custom_linalg.py ├── read-me-pic.png ├── vector_fields.py └── vector_fields_demo.ipynb /.gitignore: -------------------------------------------------------------------------------- 1 | .ipynb_checkpoints/ 2 | __pycache__/ 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Nadia 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vector-fields 2 | #### Artistic visualization of vector fields created with Matplotlib and Jupyter Notebook 3 | 4 | 5 | Main code and beautiful pictures are in [vector_fields_demo.ipynb](vector_fields_demo.ipynb) 6 | 7 | 8 | 9 | ![This is an image](read-me-pic.png) 10 | -------------------------------------------------------------------------------- /custom_linalg.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from numpy import linalg as la 3 | 4 | def rotation_matrix_by_2_vec(a, b): 5 | unit_vector_a = a / np.linalg.norm(a) 6 | unit_vector_b = b / np.linalg.norm(b) 7 | cos_t = np.dot(unit_vector_a, unit_vector_b) 8 | sin_t = np.sqrt(1-cos_t**2)*np.sign(np.cross(unit_vector_a, unit_vector_b)) 9 | return np.array([[cos_t, -sin_t], [sin_t, cos_t]]) 10 | 11 | def rotation_matrix_by_angle(t): 12 | cos_t = np.cos(t) 13 | sin_t = np.sin(t) 14 | return np.array([[cos_t, -sin_t], [sin_t, cos_t]]) -------------------------------------------------------------------------------- /read-me-pic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sleeping-h/vector-fields/b2881bbc23062a08c9b7e1118df5a55f768014f7/read-me-pic.png -------------------------------------------------------------------------------- /vector_fields.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from numpy import linalg as la 3 | import matplotlib.pyplot as plt 4 | 5 | from custom_linalg import rotation_matrix_by_angle 6 | 7 | 8 | class BaseField: 9 | max_shift = .01 10 | 11 | def direction(self, p): 12 | ''' returns unit vector ''' 13 | v = self._vector(p) 14 | return v / (la.norm(v) + .000001) 15 | 16 | def vector(self, p): 17 | return self._vector(p) 18 | 19 | def _vector(self, p): 20 | return 1, 0 21 | 22 | def point_close_to_source(self, p): 23 | return False 24 | 25 | def compute_traces(self, starting_points, trace_segments=100, dv=.02): 26 | ''' main logic here ''' 27 | num_traces = len(starting_points) 28 | traces = np.zeros((num_traces, trace_segments, 2)) 29 | traces[:,0,:] = starting_points 30 | for t in range(trace_segments - 1): 31 | for i in range(num_traces): 32 | if self.point_close_to_source(traces[i, t]): 33 | traces[i, t + 1] = traces[i, t] 34 | continue 35 | delta = dv * self.vector(traces[i, t]) 36 | if la.norm(delta) > self.max_shift: 37 | delta = self.direction(traces[i, t]) * self.max_shift 38 | traces[i, t + 1] = traces[i, t] + delta 39 | return traces 40 | 41 | 42 | class ExpandingField(BaseField): 43 | def _vector(self, p): 44 | return p 45 | 46 | 47 | class CosineField(BaseField): 48 | def _vector(self, p): 49 | x, y = p 50 | v = np.array((np.cos(3 * (x + 2 * y)), np.sin(3 * (x - 2 * y)))) 51 | return 100 * v 52 | 53 | 54 | class TwoCuspsField(BaseField): 55 | def _vector(self, p): 56 | x, y = p 57 | v = np.array((x**2 + 2 * x * y, y**2 + 2 * x * y)) 58 | return 100 * v 59 | 60 | 61 | class DipoleField(BaseField): 62 | ''' someone on Internet said this is expression for dipole field ''' 63 | def _vector(self, p): 64 | x, y = 10 * p 65 | v = np.array(((x + 1) / ((y + 1)**2 + y**2) - (x - 1) / ((x - 1)**2 + y**2), 66 | y / ((y + 1)**2 + x**2) - y / ((x - 1)**2 + y**2) 67 | )) 68 | return 100 * v 69 | 70 | 71 | class CurlField(BaseField): 72 | ''' CurlField is a compostion of spiral fields produced by sources. 73 | `sources` is a tuple of tuples with coordinates of some 'particles' 74 | and direction of spiral (in radians) relative to source position ''' 75 | sources = ((np.array((.1, .2)), np.pi/2), 76 | (np.array((.1, .9)), np.pi/6), 77 | (np.array((.7, .9)), 0)) 78 | 79 | def forces(self, p): 80 | return [( (p - src) @ rotation_matrix_by_angle(angle) ) 81 | / (la.norm(p - src) ** 2 + 0.0001) 82 | for src, angle in self.sources] 83 | 84 | def __init__(self, sources=None): 85 | if sources: 86 | self.sources = sources 87 | 88 | def _vector(self, p): 89 | return sum(self.forces(p), np.array([0, 0])) 90 | 91 | def point_close_to_source(self, p): 92 | for src, _ in self.sources: 93 | if la.norm(src - p) < .005: 94 | return True 95 | return False 96 | 97 | 98 | class DivCurlField(BaseField): 99 | ''' this was initial version of CurlField ''' 100 | 101 | sources = ((np.array((.6, .2)), 'curl', 1), 102 | (np.array((.2, .9)), 'div', .5)) 103 | 104 | def forces(self, p): 105 | rotation = { 106 | 'curl': np.array([[0, -1], [1, 0]]), 107 | 'div': np.identity(2), 108 | } 109 | return [( (p - src) @ rotation[_type] * mass ) 110 | / ( la.norm(p - src)**2 + .001 ) 111 | for src, _type, mass in self.sources] 112 | 113 | def __init__(self, sources=None): 114 | if sources: 115 | self.sources = sources 116 | 117 | def _vector(self, p): 118 | return sum(self.forces(p), np.array([0, 0])) 119 | 120 | 121 | fields = (ExpandingField, 122 | CosineField, 123 | TwoCuspsField, 124 | DipoleField, 125 | DivCurlField, 126 | CurlField, 127 | ) 128 | 129 | 130 | def preview_flow(field, n_traces=100, trace_segments=15, 131 | dv=.01, dots=False, starting_points=None, subplot=None): 132 | if not subplot: 133 | _, subplot = plt.subplots() 134 | setup_empty_subplot(subplot, field.__class__.__name__) 135 | if not starting_points: 136 | starting_points = np.random.rand(n_traces, 2) - np.array((.5, .5)) 137 | traces = field.compute_traces(starting_points, trace_segments, dv=dv) 138 | for trace in traces: 139 | subplot.plot(*trace.T, color='grey') 140 | if dots: 141 | subplot.scatter(*traces[:,0,:].T, color='black', s=3) 142 | 143 | 144 | def setup_empty_subplot(subplot, title=None): 145 | subplot.axis('equal') 146 | subplot.set_title(title) 147 | --------------------------------------------------------------------------------