├── .gitignore ├── LICENSE ├── README.md ├── SConstruct ├── basics ├── dna ├── dragon.py ├── fractal_helper ├── SConscript ├── __init__.py └── module.cpp ├── l-system ├── laplace ├── lorenz ├── mitsuba.py ├── mview ├── noise ├── notile ├── .gitignore ├── SConscript └── notile.tex ├── poincare ├── poincare-instance ├── poincare-instance-sep ├── poincare-laser ├── poker ├── render-dragon ├── setup.cfg ├── sierpinski └── union /.gitignore: -------------------------------------------------------------------------------- 1 | *.png 2 | *.svg 3 | *.pyc 4 | *.obj 5 | *.gif 6 | *.so 7 | mitsuba.*.log 8 | gen 9 | gen-* 10 | config.py 11 | .sconsign.dblite 12 | .sconf_temp 13 | config.log 14 | build 15 | poincare-checkpoint-*.obj 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2012, Geoffrey Irving, Henry Segerman, Martin Wicke, Otherlab. 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 8 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | 3. The names of the authors may not be used to endorse or promote products derived from this software without specific prior written permission. 10 | 11 | THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Various fractal scripts 2 | ======================= 3 | 4 | `fractal` contains scripts for a few different types of fractals, plus a bit of non-fractal 5 | mathematical exploration. The repo started as a tutorial for the Python programming language 6 | inside Otherlab. In addition to very simple examples (L-system curves, noise curves, and a 7 | poker-based fractal), it contains code to generated so-called "developing fractal curves" 8 | illustrating the refinement of L-system curves as a smooth surface: 9 | 10 | Geoffrey Irving and Henry Segerman, "Developing fractal curves" (in review). 11 | 12 | The license is standard three-clause BSD (see the included `LICENSE` file or 13 | [LICENSE](https://github.com/otherlab/fractal/blob/master/LICENSE)). 14 | 15 | ### Dependencies 16 | 17 | The simple pure Python fractals depend on 18 | 19 | * [python >= 2.6](http://python.org): A scripting language 20 | * [numpy >= 1.5](http://numpy.scipy.org): Efficient multidimensional arrays for Python 21 | * [scipy](http://www.scipy.org): Scientific computation for Python 22 | * [matplotlib](http://matplotlib.sourceforge.net): Python plotting 23 | 24 | Mixed Python/C++ code such as developing fractal curves (`dragon.py` and `render-dragon`) 25 | additionally depend directly on 26 | 27 | * [geode](https://github.com/otherlab/geode): Otherlab computational geometry library 28 | * other/gui: Otherlab gui library 29 | * [mitsuba >= 0.4.1](http://www.mitsuba-renderer.org): Physically based rendering 30 | 31 | `geode` has a few more indirect dependencies (boost, scons). 32 | 33 | Unfortunately, `other/gui` is not yet open source, so the interactive features of `dragon.py` 34 | and other 3D interactive scripts will not work outside of Otherlab. We will be releasing 35 | and open source version soon, at which point `fractal` will be updated accordingly. Note that 36 | `dragon.py` can be run in console mode without `other/gui` and then visualized and rendered 37 | via Mitsuba. 38 | 39 | ### Setup 40 | 41 | The simple scripts can be run immediately if their dependencies are available. For scripts 42 | which use C++ (anything that fails on `import geode` or `import fractal_helper`), first 43 | install `geode` via the instructions at https://github.com/otherlab/geode. Then build the 44 | C++ components of `fractal` via 45 | 46 | git clone https://github.com/otherlab/fractal.git 47 | cd fractal 48 | ln -s /config.py # If geode required configuration 49 | scons -j 5 50 | 51 | If `geode` was built in place and not installed, add the following lines to `config.py` to 52 | tell `fractal` where to find it: 53 | 54 | # In config.py 55 | geode_dir = '' 56 | geode_include = [geode_dir] 57 | geode_libpath = [geode_dir+'/build/$arch/$type/lib'] 58 | 59 | ### Simple scripts 60 | 61 | The simple scripts are run as follows: 62 | 63 | cd other/fractal 64 | ./basics 65 | ./poker 66 | ./laplace 67 | ./noise 68 | ./l-system # List available kinds 69 | ./l-system koch # Visualize a Koch snowflake 70 | 71 | ### Developing fractal curves 72 | 73 | If `other/gui` is available, `dragon.py` can be run without arguments and all parameters 74 | adjusted interactively. Otherwise, it can run in console mode with `--console 1` to dump 75 | out data for visualization in other programs. There are two modes: instanced (the default) 76 | and single mesh, controlled with `--instanced `. Single mesh mode generates a single 77 | smooth surface (suitable for 3D printing with an appropriate `--thickness` value). Instanced 78 | mode splits the surface into self-similar patches for efficient rendering in Mitsuba, with 79 | the benefit that each patch isomophism class can be given a different pretty color. See 80 | `render-dragon` for the commands used in the "Developing fractal curves" paper. Here is one 81 | example: 82 | 83 | # Generate a level 13 dragon curve, writing mitsuba data to gen-dragon, and view it with Mitsuba 84 | ./dragon.py --type dragon --level 13 --smooth 4 --border-layers 4 --thickness 0.2 --ground 1 --rotation=0,-1,0,0 --mitsuba-dir gen-dragon --console 1 85 | ./render-dragon --gui 1 --view front --data gen-dragon 86 | 87 | # Generate a level 11 dragon curve, writing a single mesh to dragon.stl 88 | ./dragon.py --type dragon --level 11 --smooth 3 --border-crease 0 --thickness 0.8 --thickness-alpha .8 --instance 0 -o dragon.stl --console 1 89 | 90 | ### Hyperbolic tesselations 91 | 92 | To see a triangular tiling of part of hyperbolic space represented in the Poincare disk model 93 | (https://en.wikipedia.org/wiki/Poincare_disk), run 94 | 95 | ./poincare --help # For available options 96 | ./poincare 97 | ./poincare --degree 8 --level 4 98 | 99 | To (attempt to) discretely isometrically embed the tiling in 3D Euclidean space, run 100 | 101 | ./poincare --mode flop 102 | 103 | This will use `other/gui` if available and fall back to matplotlib otherwise (in which case it will be 104 | impossible to change options after startup). To save the mesh to a file on startup, use 105 | 106 | ./poincare --mode flop --autosave poincare.obj 107 | -------------------------------------------------------------------------------- /SConstruct: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import sys 4 | import glob 5 | import subprocess 6 | 7 | def die(message): 8 | print>>sys.stderr,'fatal:',message 9 | sys.exit(1) 10 | 11 | # Require a recent version of scons 12 | EnsureSConsVersion(2,0,0) 13 | 14 | # Read configuration from config.py. 15 | # As far as I can tell, the scons version of this doesn't allow defining new options in SConscript files. 16 | sys.path.insert(0,Dir('#').abspath) 17 | 18 | try: 19 | import config 20 | has_config = True 21 | except ImportError: 22 | has_config = False 23 | 24 | del sys.path[0] 25 | def options(env,*vars): 26 | for name,help,default in vars: 27 | Help('%s: %s (default %s)\n'%(name,help,default)) 28 | if name in ARGUMENTS: 29 | value = ARGUMENTS[name] 30 | try: 31 | value = int(value) 32 | except ValueError: 33 | pass 34 | env[name] = value 35 | elif has_config: 36 | env[name] = config.__dict__.get(name,default) 37 | else: 38 | env[name] = default 39 | 40 | # Base environment 41 | env = Environment(tools=['default','pdflatex','pdftex','textfile'],TARGET_ARCH='x86') # TARGET_ARCH needed on Windows 42 | default_cxx = env['CXX'] 43 | posix = env['PLATFORM']=='posix' 44 | darwin = env['PLATFORM']=='darwin' 45 | windows = env['PLATFORM']=='win32' 46 | if not windows: 47 | env.Replace(ENV=os.environ) 48 | verbose = True 49 | 50 | # Default base directory 51 | if darwin: 52 | for base in '/opt/local','/usr/local': 53 | if os.path.exists(base): 54 | env.Replace(base=base) 55 | env.Append(CPPPATH=[base+'/include'],LIBPATH=[base+'/lib']) 56 | break 57 | else: 58 | die("We're on Mac, but neither /opt/local or /usr/local exists. Geode requires either MacPorts or Homebrew.") 59 | else: 60 | env.Replace(base='/usr') 61 | 62 | # Add install or develop targets only if explicitly asked for on the command line 63 | env['install'] = 'install' in COMMAND_LINE_TARGETS 64 | env['develop'] = 'develop' in COMMAND_LINE_TARGETS 65 | if env['develop']: 66 | env.Alias('develop',[]) 67 | 68 | # Base options 69 | options(env, 70 | ('cxx','C++ compiler',''), 71 | ('arch','Architecture (e.g. opteron, nocona, powerpc, native)','native'), 72 | ('type','Type of build (e.g. release, debug, profile)','release'), 73 | ('default_arch','Architecture that doesn\'t need a suffix',''), 74 | ('cache','Cache directory to use',''), 75 | ('shared','Build shared libraries',1), 76 | ('shared_objects','Build shareable objects when without shared libraries',1), 77 | ('real','Primary floating point type (float or double)','double'), 78 | ('install_programs','install programs into source directories',1), 79 | ('Werror','turn warnings into errors',1), 80 | ('Wconversion','warn about various conversion issues',0), 81 | ('hidden','make symbols invisible by default',0), 82 | ('openmp','use openmp',1), 83 | ('syntax','check syntax only',0), 84 | ('use_latex','Use latex (mostly for internal technical documents)',0), 85 | ('thread_safe','use thread safe reference counting in pure C++ code',1), 86 | ('optimizations','override default optimization settings',''), 87 | ('sse','Use SSE if available',1), 88 | ('skip','list of modules to skip',[]), 89 | ('skip_libs', 'list of libraries to skip', []), 90 | ('skip_programs','Build libraries only',0), 91 | ('base','Standard base directory for headers and libraries',env['base']), 92 | ('cxxflags_extra','',[]), 93 | ('linkflags_extra','',[]), 94 | ('cpppath_extra','',['/usr/local/include']), 95 | ('libpath_extra','',['/usr/local/lib']), 96 | ('rpath_extra','',[]), 97 | ('libs_extra','',[]), 98 | ('prefix','Path to install libraries, binaries, and scripts','/usr/local'), 99 | ('prefix_include','Override path to install headers','$prefix/include'), 100 | ('prefix_lib','Override path to install libraries','$prefix/lib'), 101 | ('prefix_bin','Override path to install binaries','$prefix/bin'), 102 | ('prefix_share','Override path to install resources','$prefix/share'), 103 | ('boost_lib_suffix','Suffix to add to each boost library','-mt'), 104 | ('python','Python executable','python'), 105 | ('mpicc','MPI wrapper compiler (used only to extract flags)',''), 106 | ('qtdir','Top level Qt dir (autodetect by default)','')) 107 | assert env['real'] in ('float','double') 108 | 109 | # Make a pristine environment for latex use 110 | latex_env = env.Clone() 111 | 112 | # Extra flag options 113 | env.Append(CXXFLAGS=env['cxxflags_extra']) 114 | env.Append(LINKFLAGS=env['linkflags_extra']) 115 | env.Append(CPPPATH=env['cpppath_extra']) 116 | env.Append(LIBPATH=env['libpath_extra']) 117 | env.Append(RPATH=env['rpath_extra']) 118 | env.Append(LIBS=env['libs_extra']) 119 | 120 | # Improve performance 121 | env.Decider('MD5-timestamp') 122 | env.SetOption('max_drift',100) 123 | env.SetDefault(CPPPATH_HIDDEN=[]) # directories in CPPPATH_HIDDEN won't be searched for dependencies 124 | env.SetDefault(CPPDEFINES=[]) 125 | env.Replace(_CPPINCFLAGS=env['_CPPINCFLAGS']+re.sub(r'\$\( (.*)\bCPPPATH\b(.*)\$\)',r'\1CPPPATH_HIDDEN\2',env['_CPPINCFLAGS'])) 126 | 127 | # Pick compiler if the user requested the default 128 | if env['cxx']=='': 129 | if windows: 130 | env.Replace(cxx=default_cxx) 131 | else: 132 | for gcc in 'clang++ g++-4.7 g++-4.6 g++'.split(): 133 | if subprocess.Popen(['which',gcc], stdout=subprocess.PIPE).communicate()[0]: 134 | env.Replace(cxx=gcc) 135 | break 136 | else: 137 | die('no suitable version of g++ found') 138 | env.Replace(CXX=env['cxx']) 139 | 140 | # If we're using gcc, insist on 4.6 or higher 141 | if re.match(r'\bg\+\+',env['cxx']): 142 | version = subprocess.Popen([env['cxx'],'--version'], stdout=subprocess.PIPE).communicate()[0] 143 | m = re.search(r'\s+([\d\.]+)(\s+|\n|$)',version) 144 | if not m: 145 | die('weird version line: %s'%version[:-1]) 146 | version_tuple = tuple(map(int, m.group(1).split('.'))) 147 | if version_tuple<(4,6): 148 | die('gcc 4.6 or higher is required, but %s has version %s'%(env['cxx'],m.group(1))) 149 | if version_tuple[0:2] == (4,7) and version_tuple[2] in (0,1): 150 | die('use of gcc 4.7.0 or 4.7.1 is strongly discouraged (ABI incompatabilities when mixing C++11 and other standards). %s is version %s'%(env['cxx'],m.group(1))) 151 | 152 | # Build cache 153 | if env['cache']!='': 154 | CacheDir(env['cache']) 155 | 156 | # Make a variant-independent environment for building python modules 157 | python_env = env.Clone(SHLIBPREFIX='',LINK=env['cxx']) 158 | if darwin: 159 | python_env.Replace(LDMODULESUFFIX='.so',SHLIBSUFFIX='.so') 160 | 161 | # Variant build setup 162 | env['variant_build'] = os.path.join('build',env['arch'],env['type']) 163 | env.VariantDir(env['variant_build'],'.',duplicate=0) 164 | 165 | # Compiler flags 166 | clang = bool(re.search(r'clang\b',env['cxx'])) 167 | if windows: 168 | def ifsse(s): 169 | return s if env['sse'] else '' 170 | if env['type'] in ('debug','optdebug'): 171 | env.Append(CXXFLAGS=' /Zi') 172 | if env['type'] in ('release','optdebug','profile'): 173 | env.Append(CXXFLAGS=' /O2') 174 | env.Append(CXXFLAGS=ifsse('/arch:SSE2') + ' /W3 /wd4996 /wd4267 /wd4180 /EHs',LINKFLAGS='/ignore:4221') 175 | if env['cxx'].endswith('icl') or env['cxx'].endswith('icl"'): 176 | env.Append(CXXFLAGS=' /wd2415 /wd597 /wd177') 177 | if env['type']=='debug': 178 | env.Append(CXXFLAGS=' /RTC1 /MDd',CCFLAGS=' /MDd',LINKFLAGS=' /DEBUG') 179 | else: 180 | env.Append(CXXFLAGS=' /MD') 181 | #dangerous: env.Append(LINKFLAGS='/NODEFAULTLIB:libcmtd.lib') 182 | elif env['cxx'].endswith('icc') or env['cxx'].endswith('icpc'): 183 | if env['type']=='optdebug' or env['type']=='debug': 184 | env.Append(CXXFLAGS=' -g') 185 | if env['type']=='release' or env['type']=='optdebug' or env['type']=='profile': 186 | env.Append(CXXFLAGS=' -O3') 187 | env.Append(CXXFLAGS=' -w -vec-report0 -Wall -Winit-self -Woverloaded-virtual',LINKFLAGS=' -w') 188 | else: # assume g++... 189 | gcc = True 190 | # Machine flags 191 | def ifsse(s): 192 | return s if env['sse'] else ' -mno-sse' 193 | if env['arch']=='athlon': machine_flags = ' -march=athlon-xp '+ifsse('-msse') 194 | elif env['arch']=='nocona': machine_flags = ' -march=nocona '+ifsse('-msse2') 195 | elif env['arch']=='opteron': machine_flags = ' -march=opteron '+ifsse('-msse3') 196 | elif env['arch']=='powerpc': machine_flags = '' 197 | elif env['arch']=='native': machine_flags = ' -march=native -mtune=native '+ifsse('') 198 | else: machine_flags = '' 199 | env.Append(CXXFLAGS=machine_flags) 200 | # Type specific flags 201 | if env['type']=='optdebug': env.Append(CXXFLAGS=' -g3') 202 | if env['type']=='release' or env['type']=='optdebug' or env['type']=='profile': 203 | optimizations = env['optimizations'] 204 | if optimizations=='': 205 | if env['arch']=='pentium4': optimizations = '-O2 -fexpensive-optimizations -falign-functions=4 -funroll-loops -fprefetch-loop-arrays' 206 | elif env['arch']=='pentium3': optimizations = '-O2 -fexpensive-optimizations -falign-functions=4 -funroll-loops -fprefetch-loop-arrays' 207 | elif env['arch']=='opteron': optimizations = '-O2' 208 | elif env['arch'] in ('nocona','native','powerpc'): optimizations = '-O3 -funroll-loops' 209 | env.Append(CXXFLAGS=optimizations) 210 | if not clang: 211 | env.Append(LINKFLAGS=' -dead_strip') 212 | if env['type']=='profile': env.Append(CXXFLAGS=' -pg',LINKFLAGS=' -pg') 213 | elif env['type']=='debug': env.Append(CXXFLAGS=' -g',LINKFLAGS=' -g') 214 | env.Append(CXXFLAGS=' -Wall -Winit-self -Woverloaded-virtual -Wsign-compare -fno-strict-aliasing') # -Wstrict-aliasing=2 215 | 216 | # Optionally warn about conversion issues 217 | if env['Wconversion']: 218 | env.Append(CXXFLAGS='-Wconversion -Wno-sign-conversion') 219 | 220 | # Optionally stop after syntax checking 221 | if env['syntax']: 222 | assert clang or gcc 223 | env.Append(CXXFLAGS='-fsyntax-only') 224 | # Don't rebuild files if syntax checking succeeds the first time 225 | for rule in 'CXXCOM','SHCXXCOM': 226 | env[rule] += ' && touch $TARGET' 227 | 228 | # Use c++11 229 | if not windows: 230 | if darwin: 231 | env.Append(CXXFLAGS=' -std=c++11') 232 | else: # Ubuntu 233 | env.Append(CXXFLAGS=' -std=c++0x') 234 | 235 | # Hide symbols by default if desired 236 | if env['hidden']: 237 | env.Append(CXXFLAGS=' -fvisibility=hidden',LINKFLAGS=' -fvisibility=hidden') 238 | 239 | if env['Werror']: 240 | env.Append(CXXFLAGS=(' /WX' if windows else ' -Werror')) 241 | 242 | # Relax a few warnings for clang 243 | if clang: 244 | env.Append(CXXFLAGS=' -Wno-array-bounds -Wno-unknown-pragmas -Wno-deprecated') # for Python and OpenMP, respectively 245 | 246 | # Decide whether to disable assertions (does not affect GEODE_ASSERT) 247 | if env['type']=='release' or env['type']=='profile' or env['type']=='optdebug': 248 | env.Append(CPPDEFINES=['NDEBUG']) 249 | 250 | # Enable OpenMP 251 | if env['openmp']: 252 | if windows: 253 | env.Append(CXXFLAGS=' /openmp') 254 | elif not clang: 255 | env.Append(CXXFLAGS='-fopenmp',LINKFLAGS='-fopenmp') 256 | else: 257 | print>>sys.stderr, 'Warning: clang doesn\'t know how to do OpenMP, so many things will be slower' 258 | 259 | # Work around apparent bug in variable expansion 260 | env.Replace(prefix_lib=env.subst(env['prefix_lib'])) 261 | 262 | # External libraries 263 | externals = {} 264 | lazy_externals = {} 265 | 266 | def external(env,name,lazy=1,**kwargs): 267 | '''Add an external library. See external_helper for keyword argument details.''' 268 | if name in externals or name in lazy_externals: 269 | raise RuntimeError("Trying to redefine the external %s"%name) 270 | 271 | def fail(): 272 | env['use_'+name] = 0 273 | if name in externals: 274 | del externals[name] 275 | if name in lazy_externals: 276 | del lazy_externals[name] 277 | if kwargs.get('required'): 278 | print>>sys.stderr, 'FATAL: %s is required. See config.log for error details.'%name 279 | Exit(1) 280 | 281 | # Do we want to use this external? 282 | Help('\n') 283 | options(env,('use_'+name,'Use '+name+' if available',1)) 284 | if env['use_'+name]: 285 | # Remember for lazy configuration 286 | lazy_externals[name] = env,kwargs,fail 287 | env['need_'+name] = kwargs.get('default',False) 288 | else: 289 | fail() 290 | 291 | # Some externals can't be lazy 292 | if not lazy: 293 | force_external(name) 294 | 295 | def has_external(name): 296 | if name in externals: 297 | return True 298 | force_external(name) 299 | return name in externals 300 | 301 | def force_external(name): 302 | try: 303 | env,kwargs,fail = lazy_externals[name] 304 | except KeyError: 305 | return 306 | external_helper(env,name,fail=fail,**kwargs) 307 | 308 | def external_helper(env,name,default=0,dir=0,flags=(),cxxflags='',linkflags='',cpppath=(),libpath=(),rpath=0,libs=(),publiclibs=(), 309 | copy=(),frameworkpath=(),frameworks=(),requires=(),hide=False,callback=None, 310 | headers=None,configure=None,preamble=(),body=(),required=False,fail=None): 311 | for r in requires: 312 | force_external(r) 313 | if not env['use_'+r]: 314 | if verbose: 315 | print 'disabling %s: no %s'%(name,r) 316 | fail() 317 | return 318 | 319 | lib = {'dir':dir,'flags':flags,'cxxflags':cxxflags,'linkflags':linkflags,'cpppath':cpppath,'libpath':libpath, 320 | 'rpath':rpath,'libs':libs,'copy':copy,'frameworkpath':frameworkpath,'frameworks':frameworks,'requires':requires, 321 | 'publiclibs':publiclibs,'hide':hide,'callback':callback,'name':name} 322 | del lazy_externals[name] 323 | externals[name] = lib 324 | 325 | # Make sure empty lists are copied and do not refer to the same object 326 | for n in lib: 327 | if lib[n] == (): 328 | lib[n] = [] 329 | 330 | Help('\n') 331 | options(env, 332 | (name+'_dir','Base directory for '+name,dir), 333 | (name+'_include','Include directory for '+name,0), 334 | (name+'_libpath','Library directory for '+name,0), 335 | (name+'_rpath','Extra rpath directory for '+name,0), 336 | (name+'_libs','Libraries for '+name,0), 337 | (name+'_publiclibs','Public (re-exported) libraries for '+name,0), 338 | (name+'_copy','Copy these files to the binary output directory for ' + name,0), 339 | (name+'_frameworks','Frameworks for '+name,0), 340 | (name+'_frameworkpath','Framework path for '+name,0), 341 | (name+'_cxxflags','Compiler flags for '+name,0), 342 | (name+'_linkflags','Linker flags for '+name,0), 343 | (name+'_requires','Required libraries for '+name,0), 344 | (name+'_pkgconfig','pkg-config names for '+name,0), 345 | (name+'_callback','Arbitrary environment modification callback for '+name,0)) 346 | 347 | # Absorb settings 348 | if env[name+'_pkgconfig']!=0: lib['pkg-config']=env[name+'_pkgconfig'] 349 | if 'pkg-config' in lib and lib['pkg-config']: 350 | def pkgconfig(pkg,data): 351 | return subprocess.Popen(['pkg-config',pkg,data],stdout=subprocess.PIPE,stderr=subprocess.PIPE).communicate()[0].replace('\n','') 352 | pkg = lib['pkg-config'] 353 | includes = pkgconfig(pkg,"--cflags-only-I").split() 354 | lib['cpppath'] = [x.replace("-I","") for x in includes] 355 | lib['cxxflags'] = pkgconfig(pkg,"--cflags-only-other") 356 | lib['linkflags'] = pkgconfig(pkg,"--libs") 357 | dir = env[name+'_dir'] 358 | def sanitize(path): 359 | return [path] if isinstance(path,str) else path 360 | if env[name+'_include']!=0: lib['cpppath'] = sanitize(env[name+'_include']) 361 | elif dir and not lib['cpppath']: lib['cpppath'] = [dir+'/include'] 362 | if env[name+'_libpath']!=0: lib['libpath'] = sanitize(env[name+'_libpath']) 363 | elif dir and not lib['libpath']: lib['libpath'] = [dir+'/lib'] 364 | if env[name+'_rpath']!=0: lib['rpath'] = sanitize(env[name+'_rpath']) 365 | elif lib['rpath']==0: lib['rpath'] = [Dir(d).abspath for d in lib['libpath']] 366 | if env[name+'_libs']!=0: lib['libs'] = env[name+'_libs'] 367 | if env[name+'_publiclibs']!=0: lib['publiclibs'] = env[name+'_publiclibs'] 368 | if env[name+'_frameworks']!=0: lib['frameworks'] = env[name+'_frameworks'] 369 | if env[name+'_frameworkpath']!=0: lib['frameworkpath'] = sanitize(env[name+'_frameworkpath']) 370 | if env[name+'_cxxflags']!=0: lib['cxxflags'] = env[name+'_cxxflags'] 371 | if env[name+'_linkflags']!=0: lib['linkflags'] = env[name+'_linkflags'] 372 | if env[name+'_copy']!=0: lib['copy'] = env[name+'_copy'] 373 | if env[name+'_callback']!=0: lib['callback'] = env[name+'_callback'] 374 | 375 | # Check whether the external is usable 376 | if configure is not None: 377 | has = configure if isinstance(configure,bool) else configure(env,lib) 378 | if not has: 379 | fail() 380 | else: 381 | assert headers is not None 382 | 383 | # Match the configure environment to how we'll actually build things 384 | env_conf = env.Clone() 385 | for n in externals.keys(): 386 | env_conf['need_'+n] = 0 387 | env_conf['need_'+name] = 1 388 | env_conf = link_flags(env_conf) 389 | libraries = [externals[n] for n in externals.keys() if env_conf.get('need_'+n)] 390 | def absorb(source,**kwargs): 391 | for k,v in kwargs.iteritems(): 392 | env_conf[k] = v 393 | objects_helper(env_conf,'',libraries,absorb) 394 | 395 | # Check whether the library is usable 396 | def check(context): 397 | context.Message('checking for %s: '%name) 398 | source = '\n'.join(list(preamble)+['#include <%s>'%h for h in headers]+['int main() {']+list(body)+[' return 0;','}\n']) 399 | r = context.TryLink(source,extension='.cpp') 400 | context.Result(r) 401 | return r 402 | conf = env_conf.Configure(custom_tests={'Check':check}) 403 | if not conf.Check(): 404 | fail() 405 | conf.Finish() 406 | 407 | # Library configuration. Local directories must go first or scons will try to install unexpectedly 408 | env.Prepend(CPPPATH=['#.','#$variant_build']) 409 | env.Append(CPPPATH=[env['prefix_include']],LIBPATH=[env['prefix_lib']]) 410 | env.Prepend(LIBPATH=['#$variant_build/lib']) 411 | if posix: 412 | env.Append(LINKFLAGS='-Wl,-rpath-link=$variant_build/lib') 413 | 414 | # Account for library dependencies 415 | def add_dependencies(env): 416 | libs = [] 417 | for name in tuple(lazy_externals.iterkeys()): 418 | if env.get('need_'+name): 419 | force_external(name) 420 | for name,lib in externals.iteritems(): 421 | if env.get('need_'+name): 422 | libs += lib['requires'] 423 | while libs: 424 | name = libs.pop() 425 | env['need_'+name] = 1 426 | libs.extend(externals[name]['requires']) 427 | 428 | # Linker flags 429 | all_uses = [] 430 | def link_flags(env): 431 | if not windows: 432 | env_link = env.Clone(LINK=env['cxx']) 433 | else: 434 | env_link = env.Clone() 435 | add_dependencies(env_link) 436 | workaround = env.get('need_qt',0) 437 | 438 | for name,lib in externals.items(): 439 | if env_link.get('need_'+name): 440 | all_uses.append('need_'+name) 441 | env_link.Append(LINKFLAGS=lib['linkflags'],LIBS=lib['libs'],FRAMEWORKPATH=lib['frameworkpath'],FRAMEWORKS=lib['frameworks']) 442 | if darwin: 443 | env_link.Append(LINKFLAGS=' '.join('-Xlinker -reexport-l%s'%n for n in lib['publiclibs'])) 444 | else: 445 | env_link.Append(LIBS=lib['publiclibs']) 446 | env_link.AppendUnique(LIBPATH=lib['libpath']) 447 | if workaround: # Prevent qt tool from dropping include paths when building moc files 448 | env_link.PrependUnique(CPPPATH=lib['cpppath']) 449 | if lib.has_key('rpath'): 450 | env_link.PrependUnique(RPATH=lib['rpath']) 451 | if lib['callback'] is not None: 452 | lib['callback'](env_link) 453 | return env_link 454 | 455 | # Copy necessary files into bin (necessary for dlls on windows) 456 | copied_files = set() 457 | def copy_files(env): 458 | env_copy = env.Clone() 459 | add_dependencies(env_copy) 460 | for name,lib in externals.items(): 461 | if env['need_'+name]: 462 | for cp in lib['copy']: 463 | target = env['prefix_bin']+cp 464 | if target not in copied_files: 465 | copied_files.add(target) 466 | env_copy.Install(env['prefix_bin'], cp) 467 | return env_copy 468 | 469 | # Convert sources into objects 470 | def objects_helper(env,source,libraries,builder): 471 | if type(source)!=str: return source # assume it's already an object 472 | cpppath_reversed = env['CPPPATH'][::-1] 473 | cpppath_hidden_reversed = env['CPPPATH_HIDDEN'][::-1] 474 | if darwin: 475 | frameworkpath = env['FRAMEWORKPATH'][:] 476 | frameworks = env['FRAMEWORKS'][:] 477 | else: 478 | frameworks,frameworkpath = [],[] 479 | cxxflags = str(env['CXXFLAGS']) 480 | for lib in libraries: 481 | (cpppath_hidden_reversed if lib['hide'] else cpppath_reversed).extend(lib['cpppath'][::-1]) 482 | frameworkpath.extend(lib['frameworkpath']) 483 | frameworks.extend(lib['frameworks']) 484 | cxxflags += lib['cxxflags'] 485 | return builder(source,CPPPATH=cpppath_reversed[::-1],CPPPATH_HIDDEN=cpppath_hidden_reversed[::-1],FRAMEWORKPATH=frameworkpath,FRAMEWORKS=frameworks,CXXFLAGS=cxxflags) 486 | def objects(env,sources): 487 | add_dependencies(env) 488 | libraries = [externals[name] for name in externals.keys() if env['need_'+name]] 489 | for lib in libraries: 490 | if lib['callback'] is not None: 491 | env = env.Clone() 492 | lib['callback'](env) 493 | builder = env.SharedObject if env['shared_objects'] or env['shared'] else env.StaticObject 494 | if type(sources)==list: return [objects_helper(env,source,libraries,builder) for source in sources] 495 | else: return objects_helper(env,sources,libraries,builder) 496 | 497 | # Recursively list all files beneath a directory 498 | def files(dir,skip=()): 499 | for f in os.listdir(dir): 500 | if f.startswith('.') or f in ['build', 'Debug', 'DebugStatic (Otherlab)', 'Release', 'ReleaseDLL (Otherlab)'] or f in skip: 501 | continue 502 | df = os.path.join(dir,f) 503 | if os.path.isdir(df): 504 | for c in files(df,skip): 505 | yield os.path.join(f,c) 506 | yield f 507 | 508 | # Automatic generation of library targets 509 | if windows: 510 | all_libs = [] 511 | python_libs = [] 512 | projects = [] 513 | 514 | def dir_or_file(path): 515 | return Entry(path).disambiguate() # either File or Dir 516 | 517 | # Target must be a directory! 518 | def install_or_link(env,target,src): 519 | if env['install'] or env['develop']: 520 | # Get the real location of src 521 | srcpath = dir_or_file(src).abspath 522 | if not os.path.exists(srcpath): 523 | srcpath = dir_or_file(src).srcnode().abspath 524 | if not os.path.exists(srcpath): 525 | raise RuntimeError("can't find %s in either source or build directories"%src) 526 | if env['install']: 527 | env.Alias('install',env.Install(target,srcpath)) 528 | elif env['develop']: 529 | env.Execute("d='%s' && mkdir -p $$$$d && ln -sf '%s' $$$$d"%(target,srcpath)) 530 | 531 | def library(env,name,libs=(),skip=(),extra=(),skip_all=False,no_exports=False,pyname=None): 532 | if name in env['skip_libs']: 533 | return 534 | sources = [] 535 | headers = [] 536 | skip = tuple(skip)+tuple(env['skip']) 537 | dir = Dir('.').srcnode().abspath 538 | candidates = list(files(dir,skip)) if not skip_all else [] 539 | for f in candidates + list(extra): 540 | if f.endswith('.cpp') or f.endswith('.cc') or f.endswith('.c'): 541 | sources.append(f) 542 | elif f.endswith('.h'): 543 | headers.append(f) 544 | if not sources and not headers: 545 | print 'Warning: library %s has no input source files'%name 546 | if env.get('need_qt',0): # Qt gets confused if we only set options on the builder 547 | env = env.Clone() 548 | force_external('qt') 549 | env.Append(CXXFLAGS=externals['qt']['cxxflags']) 550 | 551 | # Install headers 552 | for h in headers: 553 | install_or_link(env,os.path.join(env['prefix_include'],Dir('.').srcnode().path,os.path.dirname(h)),h) 554 | 555 | # Tell the compiler which library we're building 556 | env.Append(CPPDEFINES=['BUILDING_'+name]) 557 | 558 | sources = objects(env,sources) 559 | env = link_flags(env) 560 | env = copy_files(env) 561 | 562 | libpath = '#'+os.path.join(env['variant_build'],'lib') 563 | path = os.path.join(libpath,name) 564 | env.Append(LIBS=libs) 565 | if env['shared']: 566 | linkflags = env['LINKFLAGS'] 567 | if darwin: 568 | linkflags = '-install_name %s/${SHLIBPREFIX}%s${SHLIBSUFFIX} '%(Dir(env.subst(env['prefix_lib'])).abspath,name)+linkflags 569 | # On Windows, this will create two files: a .lib (for other builds), and a .dll for the runtime. 570 | lib = env.SharedLibrary(path,source=sources,LINKFLAGS=linkflags) 571 | else: 572 | lib = env.StaticLibrary(path,source=sources) 573 | env.Depends('.',lib) 574 | # Install dlls in bin, lib and exp in lib 575 | if windows: 576 | installed = [] 577 | for l in lib: 578 | if l.name[-4:] in ['.dll','.pyd']: 579 | installed.extend(env.Install(env['prefix_bin'],l)) 580 | elif not no_exports: 581 | installed.extend(env.Install(env['prefix_lib'],l)) 582 | lib = installed 583 | all_libs.append(lib) 584 | if 'module.cpp' in cpps: 585 | python_libs.append(lib) 586 | else: 587 | for l in lib: 588 | install_or_link(env, env['prefix_lib'], l) 589 | if env['use_python']: 590 | if pyname is None: 591 | pyname = name 592 | module = os.path.join('#'+Dir('.').srcnode().path,pyname) 593 | module = python_env.LoadableModule(module,source=[],LIBS=name,LIBPATH=[libpath]) 594 | python_env.Depends(module,lib) # scons doesn't always notice this (obvious) dependency 595 | 596 | def config_header(env,name,extra=()): 597 | """Generate a configuration header in the current directory""" 598 | env = env.Clone() 599 | add_dependencies(env) 600 | 601 | # Collect flags 602 | flags = [] 603 | if env['real']=='float': 604 | flags.append('GEODE_FLOAT') 605 | flags.append(('GEODE_THREAD_SAFE',int(env['thread_safe']))) 606 | if env['sse']: 607 | flags.append('__SSE__') 608 | if windows: 609 | if not env['shared']: 610 | flags.append('GEODE_SINGLE_LIB') 611 | flags.extend(('_CRT_SECURE_NO_DEPRECATE','NOMINMAX','_USE_MATH_DEFINES')) 612 | for n,lib in externals.items(): 613 | if env.get('need_'+n): 614 | flags.extend(lib['flags']) 615 | # Turn off boost::exceptions to avoid completely useless code bloat 616 | flags.append('BOOST_EXCEPTION_DISABLE') 617 | 618 | # Generate header 619 | lines = ['// Autogenerated configuration header','#pragma once'] 620 | for flag in flags: 621 | assert isinstance(flag,(str,bytes,tuple)),'Flags must be string or (string,value), got %s'%flag 622 | n,v = flag if isinstance(flag,tuple) else (flag,None) 623 | lines.append('\n#ifndef %s\n#define %s%s\n#endif'%(n,n,('' if v is None else ' %s'%v))) 624 | if extra: 625 | lines.append('') 626 | lines.extend(extra) 627 | header, = env.Textfile(name,lines) 628 | env.Depends('.',header) 629 | install_or_link(env,os.path.join(env['prefix_include'],os.path.dirname(header.srcnode().path)),header) 630 | 631 | # Build a program 632 | def program(env,name,cpp=None): 633 | if env['skip_programs']: 634 | return 635 | if cpp is None: 636 | cpp = name + '.cpp' 637 | env = link_flags(env) 638 | env = copy_files(env) 639 | files = objects(env,cpp) 640 | bin = env.Program('#'+os.path.join(env['variant_build'],'bin',name),files) 641 | env.Depends('.',bin) 642 | for b in bin: 643 | install_or_link(env, env['prefix_bin'], b) 644 | 645 | # Install a (possibly directory) resource 646 | def resource(env,path): 647 | # If we are installing, and we're adding a directory, add a dependency for each file found in its subtree 648 | node = dir_or_file(path).srcnode() 649 | if env['install'] and os.path.isdir(str(node)): 650 | def visitor(basedir, dirname, names): 651 | reldir = os.path.relpath(dirname, basedir) 652 | for name in names: 653 | fullname = os.path.join(dirname, name) 654 | install_or_link(env, os.path.join(env['prefix_share'], reldir), fullname) 655 | basedir = str(Dir('.').srcnode()) 656 | os.path.walk(str(node), visitor, basedir) 657 | else: 658 | # if we're just making links, a single link is enough even if it's a directory 659 | install_or_link(env, env['prefix_share'], node) 660 | 661 | # Configure latex 662 | def configure_latex(): 663 | def check(context): 664 | context.Message('checking for latex: ') 665 | r = context.TryBuild(latex_env.PDF,text=r'\documentclass{book}\begin{document}\end{document}',extension='.tex') 666 | context.Result(r) 667 | return r 668 | conf = latex_env.Configure(custom_tests={'Check':check}) 669 | if not conf.Check(): 670 | latex_env['use_latex'] = 0 671 | conf.Finish() 672 | latex_configured = False 673 | 674 | # Turn a latex document into a pdf 675 | def latex(name): 676 | if latex_env['use_latex']: 677 | global latex_configured 678 | if not latex_configured: 679 | latex_configured = True 680 | configure_latex() 681 | pdf = os.path.join('#'+Dir('.').srcnode().path,name+'.pdf') 682 | latex_env.PDF(pdf,name+'.tex') 683 | 684 | # Automatic python configuration 685 | def configure_python(env,python): 686 | pattern = re.compile(r'^\s+',flags=re.MULTILINE) 687 | data = subprocess.Popen([env['python'],'-c',pattern.sub('',''' 688 | import numpy 689 | import distutils.sysconfig as sc 690 | get = sc.get_config_var 691 | def p(s): print "'%s'"%s 692 | p(sc.get_python_inc()) 693 | p(numpy.get_include()) 694 | p(get('LIBDIR')) 695 | p(get('LDLIBRARY')) 696 | p(get('PYTHONFRAMEWORKPREFIX')) 697 | p(get('VERSION')) 698 | p(get('prefix')) 699 | ''')],stdout=subprocess.PIPE).communicate()[0] 700 | include,nmpy,libpath,lib,frameworkpath,version,prefix = [s.strip()[1:-1] for s in data.strip().split('\n')] 701 | assert include,nmpy 702 | python['cpppath'] = [include] if os.path.exists(os.path.join(include,'numpy')) else [include,nmpy] 703 | if darwin: 704 | python_config = 'python%s-config' % version 705 | python['linkflags'] = subprocess.check_output([python_config,'--ldflags']) 706 | elif windows: 707 | python['libpath'] = [prefix,os.path.join(prefix,'libs')] 708 | python['libs'] = ['python%s'%version] 709 | else: 710 | assert libpath and lib and libpath!='None' and lib!='None' 711 | python['libpath'] = [libpath] 712 | python['libs'] = [lib] 713 | return 1 714 | 715 | # Automatic MPI configuration 716 | def configure_mpi(env,mpi): 717 | # Find the right mpicc 718 | if env['mpicc']=='': 719 | if windows: 720 | env.Replace(mpicc='mpicc') 721 | else: 722 | mpicc_options = ['mpicc','openmpicc'] 723 | for mpicc in mpicc_options: 724 | if subprocess.Popen(['which',mpicc], stdout=subprocess.PIPE).communicate()[0]: 725 | env.Replace(mpicc=mpicc) 726 | break 727 | else: 728 | if verbose: 729 | print 'disabling mpi: mpicc not found' 730 | return 0 731 | 732 | # Configure MPI if it exists 733 | try: 734 | if env['mpicc'] and not mpi['cpppath']: 735 | # Find mpi.h 736 | mpi_include_options = ['/opt/local/include/openmpi','/usr/local/include/openmpi','/opt/local/include/mpi','/usr/local/include/mpi'] 737 | for dir in mpi_include_options: 738 | if os.path.exists(os.path.join(dir,'mpi.h')): 739 | mpi['cpppath'] = dir 740 | break 741 | if env['mpicc'] and not (mpi['cxxflags'] or mpi['linkflags'] or mpi['libs']): 742 | for flags,stage in ('linkflags','link'),('cxxflags','compile'): 743 | mpi[flags] = ' '+subprocess.Popen([env['mpicc'],'--showme:%s'%stage],stdout=subprocess.PIPE).communicate()[0].strip() 744 | all_flags = mpi['linkflags'].strip().split() 745 | flags = [] 746 | for f in all_flags: 747 | if f.startswith('-l'): 748 | mpi['libs'].append(f[2:]) 749 | else: 750 | flags.append(f) 751 | mpi['linkflags'] = ' '+' '.join(flags) 752 | except OSError,e: 753 | if verbose: 754 | print 'disabling mpi: %s'%e 755 | return 0 756 | return 1 757 | 758 | # Predefined external libraries 759 | external(env,'python',default=1,flags=['GEODE_PYTHON'],configure=configure_python) 760 | external(env,'boost',default=1,hide=1,headers=['boost/version.hpp']) 761 | external(env,'boost_link',requires=['boost'],libs=['boost_iostreams$boost_lib_suffix', 762 | 'boost_filesystem$boost_lib_suffix','boost_system$boost_lib_suffix','z','bz2'],hide=1,headers=()) 763 | external(env,'mpi',flags=['GEODE_MPI'],configure=configure_mpi) 764 | 765 | # BLAS is tricky. We define separate externals for openblas, atlas, and mkl, 766 | # then a unified external which picks one of them. 767 | def blas_variants(): 768 | body = [' cblas_dscal(0,1,0,1);'] 769 | external(env,'atlas',libs=['cblas','lapack','atlas'],headers=['cblas.h'],body=body) 770 | external(env,'openblas',libs=['lapack','blas'],headers=['cblas.h'],body=body) 771 | if darwin: external(env,'accelerate',frameworks=['Accelerate'],headers=['Accelerate/Accelerate.h'],body=body) 772 | if windows: external(env,'mkl',flags=['GEODE_MKL'],headers=['mkl_cblas.h'],body=body,libs='mkl_intel_lp64 mkl_intel_thread mkl_core mkl_mc iomp5 mkl_lapack'.split()) 773 | else: external(env,'mkl',flags=['GEODE_MKL'],headers=['mkl_cblas.h'],body=body,linkflags='-Wl,--start-group -lmkl_intel_lp64 -lmkl_intel_thread -lmkl_core -lmkl_mc -liomp5 -lmkl_lapack -Wl,--end-group -fopenmp -pthread') 774 | blas_variants() 775 | def configure_blas(env,blas): 776 | kinds = ['accelerate']*darwin+['openblas','atlas','mkl'] 777 | for kind in kinds: 778 | force_external(kind) 779 | if env['use_'+kind]: 780 | print 'configuring blas: using %s'%kind 781 | blas['requires'] = [kind] 782 | return 1 783 | print>>sys.stderr, "disabling blas: can't find any variant, tried %s, and %s"%(', '.join(kinds[:-1]),kinds[-1]) 784 | return 0 785 | external(env,'blas',default=1,flags=['GEODE_BLAS'],configure=configure_blas) 786 | 787 | # Descend into a child SConscript 788 | def child(env,dir): 789 | # Descent into a subdirectory 790 | if dir in env['skip']: 791 | return 792 | path = Dir(dir).path 793 | variant = env['variant_build'] 794 | if not path.startswith(variant): 795 | path = os.path.join(variant,path) 796 | env.SConscript('#'+os.path.join(path,'SConscript'),exports='env') 797 | 798 | # Descend into all child SConscripts in order of priority 799 | def children(env,skip=()): 800 | # Directories that define externals used by other directories must come first. 801 | # Therefore, we sort children with .priority files first in increase order of priority. 802 | def priority(dir): 803 | try: 804 | return float(open(File(os.path.join(dir,'.priority')).srcnode().abspath).read()) 805 | except IOError: 806 | return 1e10 807 | base = Dir('.').srcnode().abspath+'/' 808 | dirs = [s[len(base):-11] for s in glob.glob(base+'*/SConscript')] 809 | for dir in sorted(dirs,key=priority): 810 | if dir not in skip and os.path.exists(File(os.path.join(dir,'SConscript')).srcnode().abspath): 811 | child(env,dir) 812 | 813 | # Build everything 814 | Export('''child children options external has_external library objects program latex 815 | clang posix darwin windows resource install_or_link config_header''') 816 | if os.path.exists(File('#SConscript').abspath): 817 | child(env,'.') 818 | else: 819 | children(env) 820 | 821 | # If we're in msvc mode, build a toplevel solution 822 | if windows and 0: 823 | env.MSVSSolution('#windows/other'+env['MSVSPROJECTSUFFIX'],projects=projects,variant=env['type'].capitalize()) 824 | 825 | # On Windows, distinct python extension modules can't share symbols. Therefore, we 826 | # build a single large extension module with links to all the dlls. 827 | if windows and env['use_python']: 828 | if env['shared']: 829 | raise RuntimeError('Separate shared libraries do not work on windows. Switch to shared=0.') 830 | 831 | # Autogenerate a toplevel module initialization routine calling all child initialization routines 832 | def make_modules(env,target,source): 833 | libs = str(source[0]).split() 834 | open(target[0].path,'w').write('''\ 835 | // Autogenerated by SConstruct: DO NOT EDIT 836 | #define GEODE_PYTHON 837 | #define GEODE_SINGLE_LIB 838 | #include 839 | #define SUB(name) void other_init_helper_##name(); other_init_helper_##name(); 840 | GEODE_PYTHON_MODULE(other_all) { 841 | %s 842 | } 843 | '''%'\n '.join('SUB(%s)'%name for name in libs)) 844 | modules, = env.Command(os.path.join(env['variant_build'],'modules.cpp'), 845 | Value(' '.join(lib[0].name[:-4] for lib in python_libs)),[make_modules]) 846 | 847 | # Build other_all.pyd 848 | env = env.Clone(SHLIBSUFFIX='.pyd',shared=1) 849 | for use in all_uses: 850 | env[use] = 1 851 | other_all = library(env,os.path.join(env['variant_build'],'other_all'), 852 | [lib[0].name[:-4] for lib in all_libs], 853 | extra=(modules.path,),skip_all=True,no_exports=True) 854 | env.Alias('py',other_all) 855 | -------------------------------------------------------------------------------- /basics: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | '''The fractal code started as part of a tutorial on Python. This script gives 3 | a few basic Python examples before jumping into interesting fractals.''' 4 | 5 | # The first line above declares this file as an executable python script 6 | 7 | import sys # Import a module so that stuff like sys.argv works 8 | from geode import * # Pull everything in the geode module directly into our namespace 9 | 10 | print 'Hello world!' # Saul wanted this 11 | 12 | if 4 < 5: 13 | print 'true' 14 | 15 | print 'the first 10 integers =', 16 | for i in xrange(10): 17 | print i, 18 | print 19 | 20 | # Build a list 21 | x = [n*n for n in xrange(5)] 22 | print 'squares =',x,x[3] 23 | 24 | # Fibonacci numbers 25 | def fib(n): 26 | if n<2: 27 | return 1 28 | else: 29 | return fib(n-1)+fib(n-2) 30 | print 'fib = %s'%[fib(n) for n in xrange(10)] 31 | 32 | # Dictionaries 33 | d = {'a':4,'b':[5,4,6]} 34 | print 'd[\'a\'] =',d['a'] 35 | print d[1] # Here an exception is thrown 36 | -------------------------------------------------------------------------------- /dna: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import division,absolute_import 4 | from geode import * 5 | from geode.geometry.platonic import * 6 | from gui import * 7 | import subprocess 8 | import sys 9 | 10 | # Useful commands: 11 | # 12 | # convert -crop 211x112+740+231 +repage -delay 4 -loop 0 `seq -f 'capture-%03g.png' 0 149` dna-large.gif 13 | # convert -crop 211x112+740+231 +repage -resize 32x17 -delay 4 -loop 0 `seq -f 'capture-%03g.png' 0 149` dna-small.gif 14 | 15 | # Properties 16 | props = PropManager() 17 | z_scale = props.add('z_scale',.5).set_category('dna') 18 | backbone_width = props.add('backbone_width',.15).set_category('dna') 19 | base_pair_width = props.add('base_pair_width',.1).set_category('dna') 20 | periods = props.add('periods',2).set_category('dna') 21 | resolution = props.add('resolution',50).set_category('dna') 22 | spacing = props.add('spacing',10).set_category('dna') 23 | around = props.add('around',10).set_category('dna') 24 | capture_frames = props.add('capture_frames',150+10).set_category('dna') 25 | seed = props.add('seed',31833).set_category('dna') 26 | mode = props.add('mode','cylinder').set_allowed('flat cylinder'.split()).set_category('dna') 27 | 28 | # Geometry of B-DNA 29 | # For details, see http://iopscience.iop.org/1367-2630/15/9/093008/pdf/1367-2630_15_9_093008.pdf 30 | pitch = pi/180*38 31 | H_bar = tan(pitch) # tan pitch = H/(2*pi*a), H_bar = H/(2*pi) 32 | phase = pi/180*140 33 | bases_per_turn = 10.5 34 | 35 | bases = 'atgc' 36 | # http://wiki.answers.com/Q/What_is_the_color_of_the_base_pairs_in_DNA 37 | if 0: # Jellybean colors 38 | base_colors = ((0,0,1),(1,1,0),(0,1,0),(1,0,0)) 39 | backbone_colors = ((.75,0,.25),(.25,0,.75)) 40 | else: 41 | base_colors = ((.2,.1,.9),(.9,.8,0),(0,.8,.2),(.9,.2,.1)) 42 | backbone_colors = ((.9,.1,.25),(.4,.1,.55)) 43 | backbone_colors = ((.75,0,.25),(.25,0,.75)) 44 | 45 | @cache 46 | def sequence(): 47 | n = 100 48 | random.seed(seed()) 49 | return random.randint(4,size=n) 50 | 51 | @cache 52 | def helix_curve(): 53 | n = periods() 54 | t = linspace(0,2*pi*n,num=resolution()*n) 55 | X = empty((len(t),3)) 56 | s = time() 57 | X[:,0] = cos(t+s) 58 | X[:,1] = sin(t+s) 59 | X[:,2] = H_bar*t 60 | return X 61 | 62 | def span_start(i): 63 | s = time() 64 | t = 2*pi/bases_per_turn*i 65 | return asarray([cos(t+s),sin(t+s),H_bar*t]) 66 | 67 | @cache 68 | def helix_mesh(): 69 | core = helix_curve() 70 | if mode()=='cylinder': 71 | return cylinder_topology(len(core)-1,around()) 72 | elif mode()=='flat': 73 | return grid_topology(len(core)-1,1) 74 | 75 | @cache 76 | def helix_X(): 77 | core = helix_curve() 78 | if mode()=='cylinder': 79 | return revolve_around_curve(core,backbone_width(),around())[1] 80 | elif mode()=='flat': 81 | tangent = core[1:]-core[:-1] 82 | tangent = concatenate([[tangent[0]],(tangent[:-1]+tangent[1:])/2,[tangent[-1]]]) 83 | assert tangent.shape==core.shape 84 | outwards = core.copy() 85 | outwards[:,2] = 0 86 | outwards = normalized(outwards) 87 | left = normalized(cross(outwards,tangent)) 88 | X = empty((len(core),2,3)) 89 | shift = backbone_width()*left 90 | X[:,0] = core-shift 91 | X[:,1] = core+shift 92 | return X.reshape(-1,3) 93 | 94 | @cache 95 | def other_rotation(): 96 | return Rotation.from_angle_axis(phase,(0,0,1)) 97 | 98 | @cache 99 | def other_helix_X(): 100 | return other_rotation()*helix_X() 101 | 102 | def span(i,j): 103 | def span(): 104 | x0 = span_start(i) 105 | x0[:2] *= .99 106 | x1 = other_rotation()*x0 107 | mid = (x0+x1)/2 108 | R = base_pair_width() 109 | return open_cylinder_mesh(mid,(x0,x1)[j],R,around()) 110 | return cache(span) 111 | 112 | def span_scene(i,j): 113 | sp = span(i,j) 114 | color = base_colors[sequence()[i]^j] 115 | return MeshScene(props,cache(lambda:sp()[0]),cache(lambda:sp()[1]),color,color) 116 | 117 | # View 118 | app = QEApp(sys.argv,True) 119 | main = MainWindow(props) 120 | main.resize_timeline(80) 121 | main.timeline.info.set_loop(1) 122 | main.view.clear_color = zeros(3) 123 | props.get('last_frame').set(1000000) 124 | frame = props.get('frame') 125 | frame_rate = props.get('frame_rate') 126 | 127 | # Define time so that 150 frames is 2pi 128 | time = cache(lambda:2*pi/150*frame()) 129 | 130 | # Key bindings 131 | main.add_menu_item('Timeline','Play/Stop',main.timeline.info.set_play,'Ctrl+p') 132 | main.add_menu_item('Timeline','Step back',main.timeline.info.go_back,'Ctrl+< Ctrl+,') 133 | main.add_menu_item('Timeline','Step forward',main.timeline.info.go_forward,'Ctrl+.') # Possibly due to a Qt bug, 'Ctrl+> Ctrl+.' doesn't work 134 | 135 | # Video capture 136 | def capture(): 137 | print 'capturing' 138 | n = capture_frames() 139 | i = [0] 140 | def grab(): 141 | filename = 'capture-%03d.png'%i[0] 142 | print ' writing %s'%filename 143 | subprocess.check_output(['scrot','-u',filename]) 144 | i[0] += 1 145 | if i[0]==n: 146 | main.view.post_render = None 147 | main.timeline.info.set_play() 148 | main.view.post_render = grab 149 | main.timeline.info.set_play() 150 | main.add_menu_item('Capture','Capture',capture,'Ctrl+g') 151 | 152 | # Add scenes 153 | for j in 0,1: 154 | color = backbone_colors[j] 155 | main.view.add_scene('helix %d'%j,MeshScene(props,helix_mesh,(helix_X,other_helix_X)[j],color,color)) 156 | for i in xrange(int(bases_per_turn*periods())): 157 | for j in 0,1: 158 | main.view.add_scene('span %d %d'%(i,j),span_scene(i,j)) 159 | 160 | # Launch! 161 | main.init() 162 | app.run() 163 | -------------------------------------------------------------------------------- /dragon.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import division 4 | import os 5 | import sys 6 | import traceback 7 | from geode import * 8 | from gui import * 9 | from gui.show_tree import * 10 | from geode.value import parser 11 | from geode.openmesh import * 12 | from fractal_helper import * 13 | import mitsuba 14 | 15 | # Examples: 16 | # 17 | # 1. The version we printed: ./dragon.py --level 11 --smooth 3 --size 150 --thickness .7 --alpha .8 --z-scale .25 18 | # 2. Bowl with missing base: ./dragon.py --level 10 --smooth 4 --size 150 --thickness .9 --closed 1 --closed-base 0 --alpha .8 --z-scale .25 19 | # 3. Version for Eugene: ./dragon.py --level 14 --smooth 2 --size 150 --thickness .3 --z-scale .5 20 | 21 | props = PropManager() 22 | ftype = props.add('type','dragon').set_allowed('dragon terdragon koch gosper sierpinski'.split()).set_category('fractal') 23 | levels = props.add('level',4).set_category('fractal') 24 | scale_level = props.add('scale_level',-1).set_category('fractal') 25 | smooth = props.add('smooth',0).set_category('fractal') 26 | corner_shift = props.add('corner_shift',1/20).set_category('fractal') 27 | size = props.add('size',150.).set_category('fractal') 28 | thickness = props.add('thickness',0.).set_category('fractal') 29 | thickness_alpha = props.add('thickness_alpha',-1.).set_category('fractal') 30 | output = props.add('output','').set_category('fractal').set_abbrev('o') 31 | closed = props.add('closed',False).set_category('fractal') 32 | closed_base = props.add('closed_base',True).set_category('fractal') 33 | z_scale = props.add('z_scale',.5).set_category('fractal') 34 | sharp_corners = props.add('sharp_corners',False).set_category('fractal') 35 | colorize = props.add('colorize',False).set_category('fractal') 36 | color_seed = props.add('color_seed',184811).set_category('fractal') 37 | instance = props.add('instance',True).set_category('fractal') 38 | border_crease = props.add('border_crease',True).set_category('fractal') 39 | border_layers = props.add('border_layers',1).set_category('fractal') 40 | flip = props.add('flip',False).set_category('fractal') 41 | curve_debug = props.add('curve_debug',-1).set_category('fractal') 42 | two_ring = props.add('two_ring',False).set_category('fractal') 43 | rearrange = props.add('rearrange',zeros(2)).set_category('fractal').set_hidden(1) 44 | 45 | ground = props.add('ground',False).set_category('render') 46 | min_dot_override = props.add('min_dot_override',inf).set_category('render') 47 | settle_step = props.add('settle_step',.01).set_category('render') 48 | mitsuba_dir = props.add('mitsuba_dir','').set_category('render') 49 | origin = props.add('origin',(0,0,0)).set_category('render') 50 | target = props.add('target',(0,0,0)).set_category('render') 51 | rotation = props.add('rotation',Rotation.identity(3)).set_category('render') 52 | console = props.add('console',False).set_category('render').set_help('skip gui') 53 | 54 | extra_mesh_name = props.add('extra_mesh','').set_category('extra').set_help('draw an extra mesh for comparison') 55 | 56 | @cache 57 | def system(): 58 | # start angle/level, shrink factor, axiom, rules, turns 59 | if ftype()=='dragon': 60 | return pi/4,sqrt(1/2),'fx',{'x':'x-yf','y':'fx+y'},{'+':pi/2,'-':-pi/2} 61 | elif ftype()=='terdragon': 62 | return pi/6,sqrt(1/3),'f',{'f':'f-f+f'},{'+':2*pi/3,'-':-2*pi/3} 63 | elif ftype()=='koch': 64 | return 0,1/3,'f+f+fc',{'f':'f-f+f-f'},{'+':2*pi/3,'-':-pi/3} 65 | elif ftype()=='gosper': 66 | a = angle(complex(sqrt(25/28),sqrt(3/28))) 67 | return -a,sqrt(1/7),'fx',{'x':'x+yf++yf-fx--fxfx-yf+','y':'-fx+yfyf++yf+fx--fx-y'},{'+':pi/3,'-':-pi/3} 68 | elif ftype()=='sierpinski': 69 | return [pi/3,-pi/3],1/2,'xf',{'x':'yf-xf-y','y':'xf+yf+x'},{'+':pi/3,'-':-pi/3} 70 | assert 0 71 | 72 | def heights_helper(levels): 73 | heights = [] 74 | z = 0 75 | alpha = system()[1] 76 | for level in xrange(levels+1): 77 | heights.append(z) 78 | z += z_scale()*alpha**level 79 | return asarray(heights) 80 | heights = cache(lambda:heights_helper(levels())) 81 | 82 | def attach_z(xy,z): 83 | X = empty((len(xy),3)) 84 | X[:,:2] = xy 85 | X[:,2] = z 86 | return X 87 | 88 | def curves_helper(levels): 89 | closed = is_closed() 90 | debug = curve_debug() 91 | with Log.scope('curves'): 92 | # Generate fractal 93 | start,shrink,axiom,rules,turns = system() 94 | curves = iterate_L_system(start,shrink,axiom,rules,turns,levels) 95 | if debug>=0: 96 | for level,curve in enumerate(curves): 97 | dx = curve[-1]-curve[0] 98 | Log.write('level %d, dist %g, dx %s, angle %s'%(level,magnitude(dx),repr(dx),repr(vector.angle(dx)))) 99 | # Flip if desired 100 | if flip(): 101 | for curve in curves: 102 | curve[:,1] *= -1 103 | # Shift corners inwards 104 | for level,curve in enumerate(curves): 105 | Log.write('level %d, vertices %d'%(level,len(curve))) 106 | full = concatenate([[curve[-1]],curve,[curve[0]]]) if closed else curve 107 | partial = curve if closed else curve[1:-1] 108 | partial[:] += corner_shift()*(full[2:]+full[:-2]-2*full[1:-1]) 109 | if debug>=0: 110 | import pylab 111 | curve = curves[debug] 112 | pylab.plot(curve[:,0],curve[:,1]) 113 | pylab.show() 114 | return curves 115 | curves = cache(lambda:curves_helper(levels())) 116 | 117 | @cache 118 | def is_closed(): 119 | return system()[2][-1]=='c' 120 | 121 | @cache 122 | def branching(): 123 | assert levels()>0 124 | open = not is_closed() 125 | a,b = (len(curves()[1])-open),(len(curves()[0])-open) 126 | branching = a//b 127 | assert a==branching*b 128 | Log.write('branching = %d'%branching) 129 | return branching 130 | 131 | @cache 132 | def mesh(): 133 | curves_ = curves() 134 | shrink = system()[1] 135 | closed = is_closed() 136 | base = len(curves_[0]) 137 | with Log.scope('mesh'): 138 | # Assemble mesh 139 | mesh = branching_mesh(branching(),levels(),base,closed) 140 | X = concatenate([attach_z(curve,height) for curve,height in zip(curves_,heights())]) 141 | assert mesh.nodes()==len(X) 142 | assert not len(mesh.nonmanifold_nodes(True)) 143 | # Rescale 144 | if scale_level()>=0: 145 | sX = concatenate([attach_z(curve,height) for curve,height in zip(curves_helper(scale_level()),heights_helper(scale_level()))]) 146 | else: 147 | sX = X 148 | Xmin = sX.min(axis=0) 149 | Xmax = sX.max(axis=0) 150 | sizes = Xmax-Xmin 151 | scale = size()/sizes.max() 152 | Log.write('scale = %g'%scale) 153 | Log.write('sizes = %g %g %g'%(tuple(scale*sizes))) 154 | center = .5*(Xmin+Xmax) 155 | X = scale*(X-center) 156 | # Compute thickness field 157 | alpha = thickness_alpha() 158 | if alpha<0: 159 | alpha = shrink 160 | thick = hstack([repeat(alpha**level,(base+closed-1)*branching()**level+1-closed) for level in xrange(levels()+1)]) 161 | thick *= thickness()/thick[-1] 162 | if scale_level()>=0: 163 | thick *= alpha**(levels()-scale_level()) 164 | # Label patches 165 | patch = arange(len(mesh.elements)//(1+branching())).repeat(1+branching()) 166 | return mesh,X,thick,patch,(scale,center) 167 | 168 | @cache 169 | def instances(): 170 | m,X,thick,_,_ = mesh() 171 | with Log.scope('classify'): 172 | _,interior,boundary = classify_loop_patches(m,X,thick,1+branching(),two_ring()) 173 | if any(rearrange()): 174 | i,b = map(int,rearrange()) 175 | interior = interior[:i]+boundary[:b]+interior[i:]+boundary[b:] 176 | boundary = [] 177 | return interior,boundary 178 | 179 | def closed_mesh(): 180 | assert not instance() 181 | m,X,_,patch,_ = mesh() 182 | # Four rotated and translated copies of the dragon curve form a closed curve 183 | translations = (0,0),(1,0),(1,1),(0,1) 184 | scale = magnitude(X[0]-X[1]) 185 | cX = vstack([Frames(scale*hstack([t,0]),Rotation.from_angle_axis(pi/2*i,(0,0,1)))*X for i,t in enumerate(translations)]) 186 | n = len(X) 187 | tris = vstack([m.elements+n*i for i in xrange(4)]+[asarray([(1,0,2*n),(0,2*n+1,2*n)],dtype=int32)]*closed_base()) 188 | patch = concatenate([patch]*4+[[patch[-1]+1]]*2) 189 | # Weld meshes together 190 | p = ParticleTree(cX,10).remove_duplicates(1e-8) 191 | ip = empty(p.max()+1,int32) 192 | ip[p] = arange(len(p),dtype=int32) 193 | cX = cX[ip] 194 | tris = p[tris] 195 | return TriangleSoup(tris),cX,patch 196 | 197 | def smoothed_mesh(mesh,X,thick,sharp): 198 | for _ in xrange(smooth()): 199 | sub = TriangleSubdivision(mesh) 200 | if sharp: 201 | sub.corners = array([0,1],dtype=int32) 202 | mesh = sub.fine_mesh 203 | X = sub.loop_subdivide(X) 204 | if thick is not None: 205 | thick = sub.loop_subdivide(thick) 206 | return mesh,X,thick 207 | 208 | @cache 209 | def smoothed(): 210 | m,X,thick,patch = closed_mesh() if closed() else mesh()[:4] 211 | m,X,thick = smoothed_mesh(m,X,thick,sharp=sharp_corners()) 212 | patch = patch.repeat(4**smooth(),axis=0) 213 | Log.write('smoothed: triangles = %d, vertices = %d'%(len(m.elements),len(X))) 214 | return m,X,thick,patch 215 | 216 | @cache 217 | def smoothed_instances(): 218 | def smooth_instance((mesh,X,thick,frames),sharp): 219 | sm,sX,st = smoothed_mesh(mesh,X,thick,sharp) 220 | norm = sm.vertex_normals(sX) 221 | cut = TriangleSoup(sm.elements[:(1+branching())*4**smooth()]) 222 | # Rearrange so that all interior vertices come first 223 | interior = unique(cut.elements) 224 | is_interior = repeat(False,len(sX)) 225 | is_interior[interior] = True 226 | vmap = empty(len(sX),dtype=int32) 227 | vmap[interior] = arange(len(interior),dtype=int32) 228 | vmap[logical_not(is_interior)] = arange(len(interior),len(sX),dtype=int32) 229 | inv = vmap.copy() 230 | inv[vmap] = arange(len(vmap),dtype=int32) 231 | # Apply mapping 232 | cut = TriangleSoup(vmap[cut.elements]) 233 | assert cut.nodes()==len(interior) 234 | sm = TriangleSoup(vmap[sm.elements]) 235 | return cut,sm,sX[inv],st[inv],norm[inv],frames 236 | interior,boundary = instances() 237 | with Log.scope('smoothing'): 238 | interior = [smooth_instance(inst,False) for inst in interior] 239 | boundary = [smooth_instance(inst,sharp_corners() and i==0) for i,inst in enumerate(boundary)] 240 | return interior,boundary 241 | 242 | def thicken_mesh(mesh,X,thick,normals,border=None): 243 | layers = border_layers() 244 | n = len(X) 245 | m = n 246 | offset = .5*thick[...,None]*normals 247 | X = vstack([X+offset,X-offset]) 248 | if border is None: 249 | border = mesh.boundary_mesh().elements 250 | border_nodes = unique(border) 251 | if border_crease(): 252 | m = len(border_nodes) 253 | inv_border_nodes = empty(len(X),dtype=int32) 254 | inv_border_nodes[border_nodes] = arange(len(border_nodes),dtype=int32) 255 | X = vstack([X]+[(1-k/layers)*X[border_nodes]+k/layers*X[border_nodes+n] for k in xrange(layers+1)]) 256 | border = inv_border_nodes[border]+2*n 257 | border = [border+k*m for k in xrange(layers+1)] 258 | border_nodes = arange(2*n,len(X),dtype=int32) 259 | else: 260 | assert layers==1 261 | border = [border,m+border] 262 | border_nodes = hstack([border_nodes,border_nodes+n]) 263 | tris = [mesh.elements,mesh.elements[:,::-1]+n] 264 | for k in xrange(layers): 265 | tris.append(vstack([border[k].T[::-1],border[k+1][:,1]]).T) 266 | tris.append(vstack([border[k+1].T,border[k][:,0]]).T) 267 | mesh = TriangleSoup(ascontiguousarray(vstack(tris))) 268 | if 0: 269 | assert not len(mesh.nonmanifold_nodes(border_crease())) 270 | return mesh,X,border_nodes 271 | 272 | @cache 273 | def thicken(): 274 | flat,X,thick,patch = smoothed() 275 | if not thickness(): 276 | assert not len(flat.nonmanifold_nodes(True)) 277 | return flat,X,patch 278 | mesh,X,_ = thicken_mesh(flat,X,thick,flat.vertex_normals(X)) 279 | if patch is not None: 280 | border_face = boundary_edges_to_faces(flat,flat.boundary_mesh().elements) 281 | border_patch = patch[border_face] 282 | patch = concatenate([patch]*2+[border_patch]*(border_layers()+1)) 283 | return mesh,X,patch 284 | 285 | def write_mesh(filename,mesh,X,normals=None): 286 | trimesh = TriMesh() 287 | trimesh.add_vertices(X) 288 | trimesh.add_faces(mesh.elements) 289 | if normals is None: 290 | trimesh.write(filename) 291 | else: 292 | trimesh.set_vertex_normals(normals) 293 | trimesh.write_with_normals(filename) 294 | 295 | @cache 296 | def thicken_instances(): 297 | layers = border_layers() 298 | def thicken_instance((mesh,full_mesh,X,thick,normals,frames)): 299 | if thickness(): 300 | # Thicken representative with neighbors 301 | full_nodes = full_mesh.nodes() 302 | full_mesh,full_X,full_border = thicken_mesh(full_mesh,X,thick,normals) 303 | full_normals = full_mesh.vertex_normals(full_X) 304 | # Thicken representative without neighbors 305 | nodes = mesh.nodes() 306 | border = full_mesh.boundary_mesh().elements 307 | border = border[all(border 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | using namespace geode; 31 | 32 | typedef real T; 33 | typedef Vector TV2; 34 | typedef Vector TV3; 35 | using std::vector; 36 | using Log::cout; 37 | using std::endl; 38 | 39 | static Array boundary_edges_to_faces(const TriangleSoup& mesh, RawArray> edges) { 40 | Array face(edges.size(),uninit); 41 | auto incident = mesh.incident_elements(); 42 | for (const int s : range(edges.size())) { 43 | GEODE_ASSERT(incident.valid(edges[s][0])); 44 | for (const int t : incident[edges[s][0]]) 45 | if (mesh.elements[t].contains(edges[s][1])) { 46 | face[s] = t; 47 | goto found; 48 | } 49 | GEODE_ASSERT(false); 50 | found:; 51 | } 52 | return face; 53 | } 54 | 55 | static vector> iterate_L_system(NdArray start_angle_per_level, const T shrink_factor, const string& axiom, const unordered_map& rules, const unordered_map& turns, const int levels) { 56 | GEODE_ASSERT(levels>=0); 57 | GEODE_ASSERT(start_angle_per_level.rank()<=1 && start_angle_per_level.flat.size()); 58 | vector> curves; 59 | string pattern = axiom; 60 | double start_angle = 0; 61 | for (const int level : range(levels+1)) { 62 | // Trace curve 63 | TV2 X; 64 | const double step = pow(shrink_factor,level); 65 | double angle = start_angle; 66 | start_angle += start_angle_per_level.flat[level%start_angle_per_level.flat.size()]; 67 | Array curve; 68 | curve.append(X); 69 | for (char c : pattern) { 70 | if (c=='f') { 71 | X += step*polar(angle); 72 | curve.append(X); 73 | } else { 74 | auto it = turns.find(c); 75 | if (it != turns.end()) 76 | angle += it->second; 77 | } 78 | } 79 | if (pattern[pattern.size()-1]=='c') { 80 | GEODE_ASSERT(magnitude(curve[0]-curve.back())<.01*step); 81 | curve.pop(); 82 | } 83 | curves.push_back(curve); 84 | if (level == levels) 85 | break; 86 | 87 | // Refine 88 | string fine; 89 | for (char c : pattern) { 90 | auto it = rules.find(c); 91 | if (it != rules.end()) 92 | fine += it->second; 93 | else 94 | fine += c; 95 | } 96 | swap(pattern,fine); 97 | } 98 | return curves; 99 | } 100 | 101 | static Ref branching_mesh(const int branching, const int levels, const int base, bool closed) { 102 | GEODE_ASSERT(branching>=2); 103 | GEODE_ASSERT(levels>=1); 104 | const int64_t count = (base-!closed)*(1+branching)*(1-(int64_t)pow((double)branching,levels))/(1-branching); 105 | GEODE_ASSERT(0> tris; 107 | tris.preallocate(int(count)); 108 | int n = base-!closed; 109 | int lo = 0; 110 | for (int level=0;level(tris); 130 | } 131 | 132 | namespace { 133 | struct PatchInfo { 134 | bool boundary; 135 | Array> tris; 136 | Array X; 137 | Array thick; 138 | }; 139 | } 140 | 141 | typedef vector,Array,Array,Array>>> Instances; 142 | 143 | static void add_rotated_neighbors(RawArray neighbors, Hashtable& vert_map, Array& verts) { 144 | int start = -1; 145 | int score = numeric_limits::max(); 146 | for (const int a : range(neighbors.size())) { 147 | int* s = vert_map.get_pointer(neighbors[a]); 148 | if (s && score>*s) { 149 | start = a; 150 | score = *s; 151 | } 152 | } 153 | GEODE_ASSERT(start>=0); 154 | for (const int j : range(neighbors.size())) { 155 | const int a = neighbors[(start+j)%neighbors.size()]; 156 | if (!vert_map.contains(a)) { 157 | vert_map.set(a,vert_map.size()); 158 | verts.append(a); 159 | } 160 | } 161 | } 162 | 163 | // Generate one vertex per (triangle,vertex) pair, merging vertices according to edge connectivity 164 | static Tuple,Array,Array> make_manifold(const TriangleSoup& mesh, RawArray X, RawArray thick) { 165 | const auto adjacent_elements = mesh.adjacent_elements(); 166 | UnionFind union_find(3*mesh.elements.size()); 167 | for (const int t0 : range(mesh.elements.size())) { 168 | const auto nodes0 = mesh.elements[t0]; 169 | for (const int i : range(3)) { 170 | const int t1 = adjacent_elements[t0][i]; 171 | if (t1>=0) { 172 | const auto nodes1 = mesh.elements[t1]; 173 | const int j = nodes1.find(nodes0[(i+1)%3]); 174 | GEODE_ASSERT(j>=0); 175 | union_find.merge(3*t0+i,3*t1+(j+1)%3); 176 | union_find.merge(3*t0+(i+1)%3,3*t1+j); 177 | } 178 | } 179 | } 180 | Array map(union_find.size(),uninit); 181 | Array X2; 182 | Array thick2; 183 | for (const int t : range(mesh.elements.size())) 184 | for (const int i : range(3)) 185 | if (union_find.is_root(3*t+i)) { 186 | map[3*t+i] = X2.append(X[mesh.elements[t][i]]); 187 | thick2.append(thick[mesh.elements[t][i]]); 188 | } 189 | Array> tris2(mesh.elements.size(),uninit); 190 | for (const int t : range(mesh.elements.size())) 191 | for (const int i : range(3)) 192 | tris2[t][i] = map[union_find.find(3*t+i)]; 193 | return tuple(new_(tris2),X2,thick2); 194 | } 195 | 196 | static Tuple,Instances,Instances> classify_loop_patches(const TriangleSoup& mesh, RawArray X, RawArray thickness, const int count, const bool two_ring) { 197 | const T tolerance = 1e-4; 198 | GEODE_ASSERT(count>=3); 199 | GEODE_ASSERT(mesh.nodes()==X.size()); 200 | GEODE_ASSERT(mesh.nodes()==thickness.size()); 201 | const int patches = mesh.elements.size()/count; 202 | GEODE_ASSERT(mesh.elements.size()==count*patches); 203 | vector>>> reps; 204 | Array names; 205 | const auto sorted_neighbors = mesh.sorted_neighbors(); 206 | const auto adjacent_elements = mesh.adjacent_elements(); 207 | const auto incident_elements = mesh.incident_elements(); 208 | int boundary_count = 0; 209 | for (const int p : range(patches)) { 210 | PatchInfo info; 211 | // Determine transform from the first triangle 212 | info.boundary = false; 213 | for (const int i : range(count)) 214 | if (adjacent_elements[count*p+i].min()<0) { 215 | info.boundary = true; 216 | break; 217 | } 218 | const auto tri = mesh.elements[count*p]; 219 | const TV3 x0 = X[tri.y]; 220 | TV3 dx = X[tri.x]-x0; 221 | const T scale = normalize(dx), 222 | inv_scale = 1/scale; 223 | const TV3 dy = normalized((X[tri.z]-x0).projected_orthogonal_to_unit_direction(dx)), 224 | dz = cross(dx,dy); 225 | const auto transform = Matrix::translation_matrix(x0)*Matrix::from_linear(Matrix(dx,dy,dz))*Matrix::scale_matrix(scale); 226 | const auto inv_transform = transform.inverse(); 227 | // Collect our vertices 228 | Hashtable ours; 229 | Hashtable vert_map; 230 | Array verts; 231 | for (const int t : count*p+range(count)) 232 | for (const int i : mesh.elements[t]) 233 | if (ours.set(i)) 234 | vert_map.set(i,verts.append(i)); 235 | // Collect one ring vertices 236 | for (const int i : range(verts.size())) 237 | add_rotated_neighbors(sorted_neighbors[verts[i]],vert_map,verts); 238 | // Collect two ring vertices adjacent to extraordinary vertices to account for the larger stencil of modified Loop subdivision 239 | for (const int i : range(ours.size(),verts.size())) 240 | if (two_ring || sorted_neighbors[verts[i]].size()!=6) 241 | add_rotated_neighbors(sorted_neighbors[verts[i]],vert_map,verts); 242 | // Compute signature 243 | for (const int i : verts) { 244 | info.X.append(inv_transform.homogeneous_times(X[i])); 245 | info.thick.append(inv_scale*thickness[i]); 246 | } 247 | // Check for signature matches 248 | for (const int r : range(int(reps.size()))) { 249 | auto& rep = reps[r]; 250 | if (rep.x.X.size()!=info.X.size()) 251 | goto next; 252 | for (const int i : range(info.X.size())) 253 | if (sqr_magnitude(rep.x.X[i]-info.X[i])>sqr(tolerance)) 254 | goto next; 255 | rep.y.append(transform); 256 | names.append(r); 257 | goto found; 258 | next:; 259 | } 260 | { 261 | GEODE_ASSERT(reps.size()<10000); 262 | boundary_count += info.boundary; 263 | Array> transforms(1,uninit); 264 | transforms[0] = transform; 265 | names.append(int(reps.size())); 266 | // Fill in triangles 267 | Array tris; 268 | Hashtable tri_set; 269 | for (const int i : range(count)) { 270 | const int t = count*p+i; 271 | tris.append(t); 272 | tri_set.set(t); 273 | } 274 | for (const int v : verts) 275 | for (const int t : incident_elements[v]) 276 | if (tri_set.set(t)) 277 | tris.append(t); 278 | for (const int t : tris) { 279 | Vector tri; 280 | for (const int i : range(3)) { 281 | const int* p = vert_map.get_pointer(mesh.elements[t][i]); 282 | if (!p) 283 | goto skip; 284 | tri[i] = vert_map.get(mesh.elements[t][i]); 285 | } 286 | info.tris.append(tri); 287 | skip:; 288 | } 289 | reps.push_back(tuple(info,transforms)); 290 | } 291 | found:; 292 | } 293 | cout << "patches = "<(info.tris),info.X,info.thick); 301 | GEODE_ASSERT(fixed.x->nodes()==fixed.y.size()); 302 | GEODE_ASSERT(!fixed.x->nonmanifold_nodes(true).size()); 303 | auto inst = tuple(fixed.x,fixed.y,fixed.z,reps[r].y); 304 | (info.boundary?boundary:interior).push_back(inst); 305 | } 306 | 307 | // All done 308 | return tuple(names,interior,boundary); 309 | } 310 | 311 | static T min_instance_dot(RawArray X, RawArray> transforms, const TV3 up) { 312 | GEODE_ASSERT(X.size()); 313 | T min_dot = inf; 314 | for (auto A : transforms) { 315 | const T upt = dot(up,A.translation()); 316 | const TV3 Bup = A.linear().transpose_times(up); 317 | for (auto x : X) 318 | min_dot = min(min_dot,upt+dot(Bup,x)); 319 | } 320 | return min_dot; 321 | } 322 | 323 | static TV3 shift_up(TV3 up, RawArray dup) { 324 | GEODE_ASSERT(dup.size()==2); 325 | auto r = Rotation::from_rotated_vector(TV3(0,0,1),up); 326 | return (r*Rotation::from_rotation_vector(TV3(dup[0],dup[1],0)))*TV3(0,0,1); 327 | } 328 | 329 | static T settling_energy(const vector,Array,Array>>>* instances, TV3 up, RawArray dup) { 330 | up = shift_up(up,dup); 331 | // Slice as far down as possible 332 | T ground = inf; 333 | for (auto& inst : *instances) 334 | ground = min(ground,min_instance_dot(inst.y,inst.z,up)); 335 | // Which way would we fall about the center? 336 | T energy = 0; 337 | for (auto& inst : *instances) { 338 | auto areas = inst.x->vertex_areas(inst.y); 339 | for (auto t : inst.z) 340 | for (const int p : range(areas.size())) 341 | energy += areas[p]*(dot(up,t.homogeneous_times(inst.y[p]))-ground); 342 | } 343 | return energy; 344 | } 345 | 346 | static TV3 settle_instances(const vector,Array,Array>>>& instances, const TV3 up, const T step) { 347 | Array dup(2); 348 | powell(curry(settling_energy,&instances,up),dup,step,1e-5,0,20); 349 | return shift_up(up,dup); 350 | } 351 | 352 | static Tuple,Array> torus_mesh(const T R, const T r, const int N, const int n) { 353 | Array X(N*n,uninit); 354 | Array> tris(2*N*n,uninit); 355 | const T dA = 2*pi/N, 356 | da = 2*pi/n; 357 | for (const int i : range(N)) 358 | for (const int j : range(n)) { 359 | const T u = dA*i, v = da*j; 360 | const T s = R+r*cos(v); 361 | const int I = i*n+j; 362 | X[I] = vec(s*cos(u),s*sin(u),r*sin(v)); 363 | const int ii = (i+1)%n, jj = (j+1)%n; 364 | tris[2*I+0] = vec(i*n+j,ii*n+j,ii*n+jj); 365 | tris[2*I+1] = vec(i*n+j,ii*n+jj,i*n+jj); 366 | } 367 | return tuple(new_(tris),X); 368 | } 369 | 370 | static Array boundary_curve_at_height(const TriangleSoup& mesh, RawArray X, const T z) { 371 | const T tolerance = 1e-5; 372 | Array> curve; 373 | for (const auto s : mesh.boundary_mesh()->elements) 374 | if (abs(X[s.x].z-z)(curve)->polygons(); 377 | GEODE_ASSERT(curves.x.size()==0 && curves.y.size()==1); 378 | return curves.y.flat; 379 | } 380 | 381 | static Array unit_spring_energy_gradient(RawArray> edges, RawArray rest, 382 | RawArray X) { 383 | GEODE_ASSERT(edges.size()==rest.size()); 384 | Array gradient(X.size()); 385 | for (const int s : range(edges.size())) { 386 | const auto edge = edges[s]; 387 | TV3 dX = X[edge.y]-X[edge.x]; 388 | const T len = normalize(dX); 389 | const TV3 dE = (len-rest[s])*dX; 390 | gradient[edge.x] -= dE; 391 | gradient[edge.y] += dE; 392 | } 393 | return gradient; 394 | } 395 | 396 | namespace { 397 | struct SimpleCollisions : public Object { 398 | GEODE_DECLARE_TYPE(GEODE_NO_EXPORT) 399 | 400 | const Array X; 401 | const Ref> points; 402 | const Ref> edges; 403 | const Ref> faces; 404 | const T close; 405 | const bool include_faces; 406 | 407 | private: 408 | SimpleCollisions(const TriangleSoup& mesh, RawArray X0, const T close, const bool include_faces) 409 | : X(X0.copy()) 410 | , points(new_>(X,1)) 411 | , edges(new_>(mesh.segment_soup(),X,1)) 412 | , faces(new_>(mesh,X,1)) 413 | , close(close) 414 | , include_faces(include_faces) {} 415 | public: 416 | 417 | T distance_energy(const T d) const { 418 | return d IV2; 426 | typedef Vector IV3; 427 | typedef Segment Seg; 428 | typedef Triangle Tri; 429 | 430 | template struct EEVisitor { 431 | const SimplexTree& edges; 432 | RawArray> elements; 433 | RawArray X; 434 | const Visit& visit; 435 | 436 | EEVisitor(const SimplexTree& edges, RawArray X, const Visit& visit) 437 | : edges(edges), elements(edges.mesh->elements), X(X), visit(visit) {} 438 | 439 | template bool cull(A...) const { 440 | return false; 441 | } 442 | 443 | void leaf(const int n0) const {} // Edges do not intersect themselves 444 | 445 | void leaf(const int n0, const int n1) const { 446 | const auto ei = elements[edges.prims(n0)[0]], 447 | ej = elements[edges.prims(n1)[0]]; 448 | if (!(ei.contains(ej.x) || ei.contains(ej.y))) 449 | visit(ei,ej,simplex(X[ei.x],X[ei.y]),simplex(X[ej.x],X[ej.y])); 450 | } 451 | }; 452 | 453 | template struct PFVisitor { 454 | const ParticleTree& points; 455 | const SimplexTree& faces; 456 | RawArray> elements; 457 | RawArray X; 458 | const Visit& visit; 459 | 460 | PFVisitor(const ParticleTree& points, const SimplexTree& faces, RawArray X, const Visit& visit) 461 | : points(points), faces(faces), elements(faces.mesh->elements), X(X), visit(visit) {} 462 | 463 | bool cull(const int n0, const int n1) const { 464 | return false; 465 | } 466 | 467 | void leaf(const int n0, const int n1) const { 468 | const int p = points.prims(n0)[0]; 469 | const auto tri = elements[faces.prims(n1)[0]]; 470 | if (!tri.contains(p)) { 471 | const auto T = Tri(X[tri.x],X[tri.y],X[tri.z]); 472 | visit(p,tri,X[p],T); 473 | } 474 | } 475 | }; 476 | 477 | template void traverse(RawArray X, const EE& edge_edge, const PF& point_face) const { 478 | GEODE_ASSERT(X.size()==faces->mesh->nodes()); 479 | this->X.copy(X); 480 | edges->update(); 481 | double_traverse(*edges,EEVisitor(edges,X,edge_edge),close); 482 | if (include_faces) { 483 | points->update(); 484 | faces->update(); 485 | double_traverse(*points,*faces,PFVisitor(points,faces,X,point_face),close); 486 | } 487 | } 488 | 489 | T closest(RawArray X) const { 490 | T closest = inf; 491 | traverse(X, 492 | [&](const IV2 ei, const IV2 ej, const Seg si, const Seg sj) { 493 | closest = min(closest,segment_segment_distance(si,sj)); 494 | }, 495 | [&](const int p, const IV3 tri, const TV3 Xp, const Tri T) { 496 | closest = min(closest,magnitude(Xp-T.closest_point(Xp).x)); 497 | }); 498 | return closest; 499 | } 500 | 501 | T energy(RawArray X) const { 502 | T sum = 0; 503 | traverse(X, 504 | [&](const IV2 ei, const IV2 ej, const Seg si, const Seg sj) { 505 | sum += distance_energy(segment_segment_distance(si,sj)); 506 | }, 507 | [&](const int p, const IV3 tri, const TV3 Xp, const Tri T) { 508 | sum += distance_energy(magnitude(Xp-T.closest_point(Xp).x)); 509 | }); 510 | return sum; 511 | } 512 | 513 | Array gradient(RawArray X) const { 514 | Array grad(X.size()); 515 | traverse(X, 516 | [&](const IV2 ei, const IV2 ej, const Seg si, const Seg sj) { 517 | const auto I = segment_segment_distance_and_normal(simplex(X[ei.x],X[ei.y]),simplex(X[ej.x],X[ej.y])); 518 | const T d = distance_gradient(I.x); 519 | grad[ei.x] -= d*(1-I.z.x)*I.y; 520 | grad[ei.y] -= d* I.z.x *I.y; 521 | grad[ej.x] += d*(1-I.z.y)*I.y; 522 | grad[ej.y] += d* I.z.y *I.y; 523 | }, 524 | [&](const int p, const IV3 tri, const TV3 Xp, const Tri Xtri) { 525 | const auto I = Xtri.closest_point(Xp); 526 | auto N = Xp-I.x; 527 | const T d = distance_gradient(normalize(N)); 528 | grad[p] += d*N; 529 | grad[tri.x] -= d*I.y.x*N; 530 | grad[tri.y] -= d*I.y.y*N; 531 | grad[tri.z] -= d*I.y.z*N; 532 | }); 533 | return grad; 534 | } 535 | 536 | struct CollisionVisitor : public Noncopyable { 537 | const SimplexTree& edges; 538 | const SimplexTree& faces; 539 | RawArray> segs; 540 | RawArray> tris; 541 | RawArray X; 542 | int count; 543 | 544 | CollisionVisitor(const SimplexTree& edges, const SimplexTree& faces, RawArray X) 545 | : edges(edges), faces(faces), segs(edges.mesh->elements), tris(faces.mesh->elements), X(X), count(0) {} 546 | 547 | bool cull(const int n0, const int n1) const { 548 | return false; 549 | } 550 | 551 | void leaf(const int n0, const int n1) { 552 | const auto seg = segs[edges.prims(n0)[0]]; 553 | const auto tri = tris[faces.prims(n1)[0]]; 554 | if (!tri.contains(seg.x) && !tri.contains(seg.y)) { 555 | const Seg S(X[seg.x],X[seg.y]); 556 | const Tri T(X[tri.x],X[tri.y],X[tri.z]); 557 | Ray ray(S); 558 | count += T.lazy_intersection(ray); 559 | } 560 | } 561 | }; 562 | 563 | int collisions(RawArray X) const { 564 | GEODE_ASSERT(X.size()==faces->mesh->nodes()); 565 | this->X.copy(X); 566 | edges->update(); 567 | faces->update(); 568 | CollisionVisitor visit(edges,faces,X); 569 | double_traverse(*edges,*faces,visit,close); 570 | return visit.count; 571 | } 572 | 573 | Array strain_limit(RawArray restlengths, RawArray X, const T alpha) const { 574 | GEODE_ASSERT(restlengths.size()==edges->mesh->elements.size()); 575 | const auto XL = X.copy(); 576 | const Array frozen(X.size()); 577 | for (const int s : range(restlengths.size())) { 578 | const auto e = edges->mesh->elements[s]; 579 | TV3 v = X[e.y]-X[e.x]; 580 | const T L = normalize(v); 581 | const TV3 dx = .5*alpha*(restlengths[s]-L)*v; 582 | XL[e.x] -= dx; 583 | XL[e.y] += dx; 584 | } 585 | traverse(X, 586 | [&](const IV2 ei, const IV2 ej, const Seg si, const Seg sj) { 587 | const auto I = segment_segment_distance_and_normal(simplex(X[ei.x],X[ei.y]),simplex(X[ej.x],X[ej.y])); 588 | if (I.x < close) { 589 | const TV3 dx = .5*alpha*(close-I.x)*I.y; 590 | XL[ei.x] -= (1-I.z.x)*dx; 591 | XL[ei.y] -= I.z.x *dx; 592 | XL[ej.x] += (1-I.z.y)*dx; 593 | XL[ej.y] += I.z.y *dx; 594 | } 595 | }, 596 | [&](const int p, const IV3 tri, const TV3 Xp, const Tri Xtri) { 597 | const auto I = Xtri.closest_point(Xp); 598 | auto N = Xp-I.x; 599 | const T d = normalize(N); 600 | if (d < close) { 601 | const TV3 dx = .5*alpha*(close-d)*N; 602 | XL[p] += dx; 603 | XL[tri.x] -= I.y.x*dx; 604 | XL[tri.y] -= I.y.y*dx; 605 | XL[tri.z] -= I.y.z*dx; 606 | } 607 | }); 608 | return XL; 609 | } 610 | }; 611 | 612 | GEODE_DEFINE_TYPE(SimpleCollisions) 613 | } 614 | 615 | Box dihedral_angle_range(const TriangleTopology& mesh, RawField X) { 616 | Box angles; 617 | for (const auto e : mesh.halfedges()) 618 | if (!mesh.is_boundary(e)) 619 | angles.enlarge(mesh.dihedral(X,e)); 620 | return angles; 621 | } 622 | 623 | GEODE_PYTHON_MODULE(fractal_helper) { 624 | GEODE_FUNCTION(boundary_edges_to_faces) 625 | GEODE_FUNCTION(iterate_L_system) 626 | GEODE_FUNCTION(branching_mesh) 627 | GEODE_FUNCTION(classify_loop_patches) 628 | GEODE_FUNCTION(min_instance_dot) 629 | GEODE_FUNCTION(settle_instances) 630 | GEODE_FUNCTION(torus_mesh) 631 | GEODE_FUNCTION(make_manifold) 632 | GEODE_FUNCTION(boundary_curve_at_height) 633 | GEODE_FUNCTION(unit_spring_energy_gradient) 634 | GEODE_FUNCTION(dihedral_angle_range) 635 | 636 | typedef SimpleCollisions Self; 637 | Class("SimpleCollisions") 638 | .GEODE_INIT(const TriangleSoup&,RawArray,const T,bool) 639 | .GEODE_METHOD(closest) 640 | .GEODE_METHOD(energy) 641 | .GEODE_METHOD(gradient) 642 | .GEODE_METHOD(collisions) 643 | .GEODE_METHOD(strain_limit) 644 | ; 645 | } 646 | -------------------------------------------------------------------------------- /l-system: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import division 4 | from numpy import * 5 | import sys 6 | import pylab 7 | import optparse 8 | 9 | def iterate(system,start,steps): 10 | s = start 11 | for step in xrange(steps): 12 | print 'step %d, size %d'%(step,len(s)) 13 | s = ''.join(system.get(c,c) for c in s) 14 | return s 15 | 16 | def draw(turns,forward,s): 17 | theta = 0 18 | thetas = [] 19 | lengths = [] 20 | for c in s: 21 | if c in forward: 22 | thetas.append(theta) 23 | lengths.append(forward[c]) 24 | if c in turns: 25 | theta += turns[c] 26 | return cumsum(lengths*cos(thetas)),cumsum(lengths*sin(thetas)) 27 | 28 | usage = 'usage: %prog koch|triangle|hilbert|other' 29 | parser = optparse.OptionParser(usage) 30 | options,args = parser.parse_args() 31 | if len(args)!=1: 32 | parser.error('expected exactly one "kind" argument') 33 | kind, = args 34 | 35 | if kind=='koch': 36 | # http://en.wikipedia.org/wiki/Koch_snowflake#Representation_as_Lindenmayer_system 37 | axiom = 'F++F++F' 38 | system = {'F':'F-F++F-F'} 39 | turns = {'+':pi/3,'-':-pi/3} 40 | forward = {'+':1,'-':1} 41 | steps = 7 42 | elif kind=='triangle': 43 | # http://en.wikipedia.org/wiki/L-system#Example_6:_Sierpinski_triangle 44 | axiom = 'A' 45 | system = {'A':'B-A-B','B':'A+B+A'} 46 | turns = {'+':pi/3,'-':-pi/3} 47 | forward = {'+':1,'-':1} 48 | steps = 8 49 | steps = 11 50 | elif kind=='hilbert': 51 | axiom = 'A' 52 | system = {'A':'-BF+AFA+FB-','B':'+AF-BFB-FA+'} 53 | turns = {'+':pi/2, '-':-pi/2} 54 | forward = {'F':1} 55 | steps = 4 56 | elif kind=='other': 57 | axiom = 'A' 58 | system = {'A':'+FA-FA'} 59 | turns = {'+':pi/1.2, '-':-pi/1.2} 60 | forward = {'F':1,'B':-1} 61 | steps = 10 62 | else: 63 | print>>sys.stderr, 'unknown kind = %s'%kind 64 | sys.exit(1) 65 | 66 | x,y = draw(turns,forward,iterate(system,axiom,steps)) 67 | pylab.plot(x,y) 68 | pylab.show() 69 | -------------------------------------------------------------------------------- /laplace: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import division 4 | from numpy import * 5 | from matplotlib import pyplot 6 | from mpl_toolkits.mplot3d import Axes3D 7 | import scipy.linalg 8 | 9 | # We're going to solve the 2D Laplace equation on an n by n grid, with sinusoidal Dirichlet boundary conditions 10 | n = 52 11 | dx = 1/(n+1) 12 | 13 | # Pick random sinusoids. Our boundary conditions will be z(x,y) = sum_i coeffs[i]*sin(freq[i,0]*x+freq[i,1]*y) 14 | m = 10 15 | random.seed(138131) 16 | coeffs = random.randn(10) 17 | freq = 2*pi*random.randn(10,2) 18 | 19 | # Set up dense matrix, treating it initially as a rank 4 tensor. We're only going to fill in the upper triangle. 20 | A = zeros((n,n,n,n)) 21 | i = arange(n).reshape(-1,1) 22 | j = i.reshape(1,-1) 23 | A[i,j,i,j] = -4 24 | A[i[:-1],j,i[1:],j] = 1 25 | A[i,j[:,:-1],i,j[:,1:]] = 1 26 | A = A.reshape((n**2,n**2)) # Smash A down from rank 4 to rank 2 27 | 28 | # Compute the right hand side 29 | b = zeros((n,n)) 30 | x = dx*arange(n).reshape(-1,1) 31 | b[:,0] += (coeffs*sin(freq[:,0]*x)).sum(axis=-1) 32 | b[:,-1] += (coeffs*sin(freq[:,0]*x+freq[:,1])).sum(axis=-1) 33 | b[0,:] += (coeffs*sin(freq[:,1]*x)).sum(axis=-1) 34 | b[-1,:] += (coeffs*sin(freq[:,1]*x+freq[:,0])).sum(axis=-1) 35 | b = b.reshape(n**2) # Smash b down from rank 2 to rank 1 36 | 37 | # Solve the linear system, using only the upper triangle of A 38 | z = scipy.linalg.solve(-A,b,sym_pos=1) 39 | z = z.reshape(n,n) 40 | print 'z =\n%s'%z 41 | 42 | # Plot 43 | x = y = dx*arange(n) 44 | x,y = meshgrid(x,y) 45 | fig = pyplot.figure() 46 | axes = fig.add_subplot(111,projection='3d') 47 | axes.plot_surface(x,y,z,rstride=4,cstride=4) 48 | pyplot.show() 49 | -------------------------------------------------------------------------------- /lorenz: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import division 4 | from geode import * 5 | from geode.value import parser 6 | from gui import * 7 | from OpenGL import GL 8 | import scipy.integrate 9 | import sys 10 | 11 | # Properties 12 | props = PropManager() 13 | sigma = props.add('sigma',10.) 14 | rho = props.add('rho',28.) 15 | beta = props.add('beta',8/3) 16 | length = props.add('length',100) 17 | resolution = props.add('resolution',10000) 18 | skip = props.add('skip',.1) 19 | parser.parse(props,'Lorenz attractor visualization') 20 | 21 | @cache 22 | def curve(): 23 | s,p,b = sigma(),rho(),beta() 24 | def f((x,y,z),t): 25 | return s*(y-x),x*(p-z)-y,x*y-b*z 26 | random.seed(17341) 27 | X0 = .1*random.randn(3) 28 | t = linspace(0,length(),resolution()) 29 | X = scipy.integrate.odeint(f,X0,t) 30 | X = X[int(skip()*len(X)):] 31 | return X 32 | 33 | class LorenzScene(Scene): 34 | def bounding_box(self): 35 | return bounding_box(curve()) 36 | def render(self,*args): 37 | X = curve() 38 | color = wheel_color(linspace(0,1,len(X))) 39 | GL.glBegin(GL.GL_LINE_STRIP) 40 | gl_colored_vertices(color,X) 41 | GL.glEnd() 42 | 43 | @cache 44 | def mesh(): 45 | n = resolution() 46 | n = n-int(skip()*n) 47 | segs = empty((n-1,2),dtype=int32) 48 | segs[:,0] = arange(n-1) 49 | segs[:,1] = arange(n-1)+1 50 | return SegmentSoup(segs) 51 | 52 | def main(): 53 | app = QEApp(sys.argv,True) 54 | main = MainWindow(props) 55 | main.view.add_scene('lorenz',LorenzScene()) 56 | #main.view.add_scene('lorenz',MeshScene(props,mesh,curve,(1,0,0),(1,0,0))) 57 | main.init() 58 | app.run() 59 | 60 | if __name__=='__main__': 61 | main() 62 | -------------------------------------------------------------------------------- /mitsuba.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import division 4 | from geode import * 5 | from numpy import * 6 | 7 | def indent(lines): 8 | return [' '+line for line in lines] 9 | 10 | def header(): 11 | return ['',''] 12 | 13 | def concat(lists): 14 | s = [] 15 | for x in lists: 16 | s += x 17 | return s 18 | 19 | def tag(tagname,children=[],**fields): 20 | s = '<%s '%tagname + ' '.join('%s="%s"'%(k,v) for k,v in fields.iteritems()) 21 | if not children: 22 | return [s+'/>'] 23 | else: 24 | return [s+'>']+indent(concat(children))+[''%tagname] 25 | 26 | def scene_version(version,*children): 27 | return '\n'.join(header()+tag('scene',version=version,children=children)) 28 | 29 | def scene(*children): 30 | return scene_version('0.3.0',*children) 31 | 32 | field_types = {int:'integer',int64:'integer',float:'float',float64:'float',str:'string',bool:'boolean'} 33 | def fields(**fields): 34 | def convert(v): 35 | if type(v)==bool: 36 | return str(v).lower() 37 | else: 38 | return v 39 | return concat(tag(field_types[type(v)],name=n,value=convert(v)) for n,v in fields.iteritems()) 40 | 41 | def typed(name): 42 | def f(type,*children): 43 | return tag(name,type=type,children=children) 44 | return f 45 | integrator = typed('integrator') 46 | sampler = typed('sampler') 47 | film = typed('film') 48 | rfilter = typed('rfilter') 49 | shape = typed('shape') 50 | luminaire = typed('luminaire') 51 | emitter = typed('emitter') 52 | camera = typed('camera') 53 | sensor = typed('sensor') 54 | phase = typed('phase') 55 | rfilter = typed('rfilter') 56 | 57 | def transform(name,*children): 58 | return tag('transform',name='toWorld',children=children) 59 | 60 | def scale(**fields): 61 | return tag('scale',**fields) 62 | 63 | def translate(v): 64 | x,y,z = v 65 | return tag('translate',x=x,y=y,z=z) 66 | 67 | def number(name,v): 68 | return tag('float',name=name,value=v) 69 | 70 | def vector(name,v): 71 | x,y,z = v 72 | return tag('vector',name=name,x=x,y=y,z=z) 73 | 74 | def point(name,v): 75 | x,y,z = v 76 | return tag('point',name=name,x=x,y=y,z=z) 77 | 78 | def rotate(r): 79 | angle,axis = r.angle_axis() 80 | return tag('rotate',x=axis[0],y=axis[1],z=axis[2],angle=180/pi*angle) 81 | 82 | def matrix(m): 83 | assert m.shape==(4,4) 84 | return tag('matrix',value=' '.join(map(str,m.ravel()))) 85 | 86 | def comma_sep(v): 87 | return ','.join(map(str,v)) 88 | 89 | def translate(vec): 90 | return tag('translate', x = vec[0], y = vec[1], z = vec[2]) 91 | 92 | def scale(vec): 93 | return tag('scale', x = vec[0], y = vec[1], z = vec[2]) 94 | 95 | def lookAt(origin,target,up): 96 | return tag('lookAt',origin=comma_sep(origin),target=comma_sep(target),up=comma_sep(up)) 97 | 98 | def bsdf(id,type,*children): 99 | return tag('bsdf',id=id,type=type,children=children) 100 | 101 | def medium(id,type,*children): 102 | return tag('medium',id=id,name=id,type=type,children=children) 103 | 104 | def spectrum(name,value): 105 | return tag('spectrum',name=name,value=value) 106 | 107 | def blackbody(name, temp): 108 | return tag('blackbody', name=name, temperature=temp) 109 | 110 | def ref(id, name = None): 111 | if name is None: 112 | return tag('ref', id=id) 113 | else: 114 | return tag('ref', name=name, id=id) 115 | 116 | def shapegroup(id,*children): 117 | return tag('shape',type='shapegroup',id=id,children=children) 118 | 119 | def instance(id,frame): 120 | if isinstance(frame,Frames): 121 | trans = transform('toWorld',rotate(frame.r),translate(frame.t)) 122 | else: 123 | trans = transform('toWorld',matrix(frame)) 124 | return shape('instance', 125 | ref(id), 126 | trans) 127 | 128 | def instances(id,frames): 129 | return concat(instance(id,f) for f in frames) 130 | 131 | def rgb(name,color): 132 | return tag('rgb',name=name,value=comma_sep(color)) 133 | 134 | def skip(*args,**kwargs): 135 | return [] 136 | 137 | def skipif(criterion, *children): 138 | if criterion: 139 | return [] 140 | else: 141 | return concat(children) 142 | 143 | def select(criterion, list1, list2): 144 | if criterion: 145 | return list1 146 | else: 147 | return list2 148 | 149 | def include(filename): 150 | return tag('include',filename=filename) 151 | -------------------------------------------------------------------------------- /mview: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import division,print_function,unicode_literals 4 | from geode.value import parser 5 | from fractal_helper import dihedral_angle_range 6 | from geode import * 7 | import sys 8 | import re 9 | 10 | # Properties 11 | props = PropManager() 12 | meshname = props.add('mesh','').set_required(1) 13 | parser.parse(props,'One-off mesh viewer',positional=[meshname]) 14 | 15 | # Load 16 | mesh,X = read_soup(meshname()) 17 | 18 | # Print information 19 | print('vertices = %d'%len(X)) 20 | print('edges = %d'%len(mesh.segment_soup().elements)) 21 | print('faces = %d'%len(mesh.elements)) 22 | if 0: 23 | print('dihedrals = %g %g'%tuple(180/pi*asarray(dihedral_angle_range(tm)))) 24 | 25 | # View 26 | if any(X[:,2]): 27 | import gui 28 | app = gui.QEApp(sys.argv,True) 29 | main = gui.MainWindow(props) 30 | main.view.add_scene('mesh',gui.MeshScene(props,const_value(mesh),const_value(X),(.2,.2,1),(0,1,0))) 31 | main.init() 32 | app.run() 33 | else: # Use pylab in 2D 34 | import pylab 35 | from matplotlib import cm,collections 36 | X = X[:,:2] 37 | pylab.plot(X[:,0],X[:,1],'r.',markersize=20) 38 | for i in xrange(len(X)): 39 | pylab.text(X[i,0],X[i,1],'v%d'%i,fontsize=20) 40 | tris = X[mesh.elements] 41 | polys = collections.PolyCollection(tris,facecolor='lightblue') 42 | axes = pylab.axes() 43 | axes.add_collection(polys) 44 | axes.set_aspect('equal') 45 | axes.set_xlim(X[:,0].min()-.1,X[:,0].max()+.1) 46 | axes.set_ylim(X[:,1].min()-.1,X[:,1].max()+.1) 47 | pylab.title(meshname()) 48 | pylab.show() 49 | -------------------------------------------------------------------------------- /noise: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import division 4 | from numpy import * 5 | import pylab 6 | 7 | n = 1000 8 | tt = arange(n)/(n-1) 9 | m = 1000 10 | 11 | # We're going to compute y(t) = sum_i c_i/(i+1) sin(2*pi*i*t), where c_i are random coefficients 12 | c = random.randn(m) 13 | 14 | def slow(): 15 | # Extremely slow method: do everything with python loops 16 | yy = [] # Start with an empty list 17 | for t in tt: # For each value of the independent variable 18 | y = 0 19 | for i in xrange(m): # for i = 0, 1, ... m-1 20 | y += c[i]/(i+1)*sin(2*pi*i*t) 21 | yy.append(y) 22 | return yy 23 | 24 | def fast(): 25 | # Much faster method: do the loop over i in python, but loop over tt with numpy 26 | yy = zeros_like(tt) 27 | for i in xrange(m): 28 | yy += c[i]/(i+1)*sin(2*pi*i*tt) 29 | return yy 30 | 31 | def fastest(): 32 | i = arange(m).reshape(-1,1) 33 | return (c.reshape(-1,1)/(i+1)*sin(2*pi*i*tt)).sum(axis=0) 34 | 35 | pylab.title('Three implementations of the same function') 36 | pylab.plot(tt,slow(),'r',label='slow') 37 | print 'finished slow' 38 | pylab.plot(tt,fast()+.001,'g',label='fast') 39 | print 'finished fast' 40 | pylab.plot(tt,fastest()+.002,'b',label='fastest') 41 | print 'finished fastest' 42 | pylab.legend() 43 | pylab.show() 44 | -------------------------------------------------------------------------------- /notile/.gitignore: -------------------------------------------------------------------------------- 1 | notile.pdf 2 | *.aux 3 | *.log 4 | *.fls 5 | *.out 6 | -------------------------------------------------------------------------------- /notile/SConscript: -------------------------------------------------------------------------------- 1 | Import('latex') 2 | 3 | latex('notile') 4 | -------------------------------------------------------------------------------- /notile/notile.tex: -------------------------------------------------------------------------------- 1 | \documentclass[11pt]{article} 2 | \usepackage{amsfonts,amssymb,amsthm,eucal,amsmath} 3 | \usepackage{graphicx} 4 | \usepackage[T1]{fontenc} 5 | \usepackage{latexsym,url} 6 | \usepackage{array} 7 | \usepackage{subfig} 8 | \usepackage{comment} 9 | \usepackage{color} 10 | \usepackage{tikz} 11 | \usepackage{cprotect} 12 | \usepackage{hyperref} 13 | \usepackage[nameinlink,noabbrev]{cleveref} 14 | 15 | \hypersetup{colorlinks=true,linkcolor=blue} 16 | \newcommand{\myspace}{\vspace{.1in}\noindent} 17 | \newcommand{\mymyspace}{\vspace{.1in}} 18 | \usepackage[inner=30mm, outer=30mm, textheight=225mm]{geometry} 19 | \creflabelformat{equation}{#2(#1)#3} 20 | \crefname{equation}{}{} 21 | \Crefname{equation}{}{} 22 | 23 | \newtheorem{theorem}{Theorem}[section] 24 | \newtheorem{prop}[theorem]{Proposition} 25 | \newtheorem{corollary}[theorem]{Corollary} 26 | \newtheorem{defn}[theorem]{Definition} 27 | \newtheorem{notn}[theorem]{Notation} 28 | \newtheorem{cond}[theorem]{Condition} 29 | \newtheorem{lemma}[theorem]{Lemma} 30 | \newtheorem{ex}[theorem]{Example} 31 | \newtheorem{rmk}[theorem]{Remark} 32 | \newcommand{\co}{\negthinspace :} 33 | \newcommand{\N}{\mathbb{N}} 34 | \newcommand{\Z}{\mathbb{Z}} 35 | \newcommand{\R}{\mathbb{R}} 36 | \newcommand{\E}{\mathbb{E}} 37 | \newcommand{\C}{\mathbb{C}} 38 | \newcommand{\CP}{\mathbb{CP}} 39 | \newcommand{\PSL}{\mathrm{PSL}_2(\mathbb{C})} 40 | \newcommand{\area}{\operatorname{area}} 41 | \newcommand{\diag}{\operatorname{diag}} 42 | \newcommand{\nt}{\negthinspace} 43 | \newcommand{\TODO}{{\color{red} TODO}} 44 | 45 | \title{Do hyperbolic tilings isometrically embed in $\R^3$?} 46 | \author{Geoffrey Irving\thanks{Email: \{irving\}@naml.us, Otherlab, San Francisco, CA, United States}} 47 | \date{Version 1, \today} 48 | 49 | \begin{document} 50 | \maketitle 51 | 52 | \begin{abstract} 53 | Do hyperbolic triangulations and other tilings isometrically embed in 3-space? 54 | Henry says this is unresolved, and may have been considered by Thurston. 55 | However, it seems like there is an amazing amount of slack in the result, so 56 | here are some thoughts. 57 | \end{abstract} 58 | 59 | \section{Hyperbolic triangulations} 60 | 61 | Let $T$ be the infinite valence 7 hyperbolic triangulation where each edge has length 1. 62 | Let $T_n$ be the radius $n$ portion of $T$ centered at a fixed origin vertex $0$. Due to negative 63 | curvature, the number of triangles in $T_n$ is at least $\alpha^n$ for some $\alpha > 1$. 64 | 65 | \section{Isometric embeddings} 66 | 67 | Assume $T$ has an isometric embedding into $\R^3$, where each triangle is mapped to a unit length flat 68 | equilateral triangle. 69 | 70 | Given a vertex-triangle pair $(v,t)$ with $v \in t \in T$, let $f(v,t) \in \R^9$ be the three vertices 71 | of triangle $t$ in order starting at $v$ (all triangles are oriented since the hyperbolic plane is 72 | orientable). Consider the image $f(T_n) \subset \R^9$: since $T_n$ has radius $\le n$, $f(T_n)$ must 73 | lie in ball of radius $3n+O(1)$, which therefore has volume at most $\beta (3n+O(1))^9$ where $\beta$ 74 | is the volume of the unit 9-sphere. 75 | 76 | Now define 77 | $$d(n,k) = \sup \left\{ d | \exists q \in \R^9, vt_1 \ne \ldots \ne vt_k \in T_n . \left|f(vt_i) - q\right| < d \right\}$$ 78 | Placing a radius $d(n,k)/2$ 9-sphere around each $f(vt)$ gives the volume inequality 79 | $$\beta d(n,k)^9 \alpha^n / k < \tau \beta (3n+O(1))^9$$ 80 | where the extra factor of $\tau$ is needed to weed out parts of the little balls that go outside 81 | the large ball and the fact of two in $d(n,k)/2$. Solving (and changing $\tau$), we have 82 | $$d(n,k) < \tau k^{1/9} n \alpha^{-n}.$$ 83 | 84 | If we instead fix a separation distance $d(n,k) = \epsilon$, solving for $k$ gives 85 | $$k > \tau_\epsilon \frac{\alpha^n}{n^9}$$ 86 | Thus, for an arbitrarily small notion of closeness, there is an exponentially large set of close triangles 87 | at most a linear geodesic distance apart. Call the largest such set $P = P(\epsilon,n)$. 88 | Consider minimal geodesic paths $\gamma_{ij}$ between all pairs of centers of triangles in $P$. 89 | 90 | The next thing to do 91 | is to analyze the space swept out by the triangles along these paths, in an attempt to prove that the normals 92 | along at least most of the paths stay close to the source and destination normals (which are all close). 93 | And the next thing after that is to apply some sort of intermediate value theorem to show that this is impossible: 94 | either there must be intersections between the triangles as they change height, or we've almost embedded a chunk of 95 | hyperbolic space into $\R^2$. 96 | 97 | I think the second of the above conjectures is easier than the first. Our plan of attack on the first is to 98 | define some notion of nonintersecting volume which becomes significantly nonzero if a geodesic path rotates 99 | out of the plane, and then prove that this volume blows up impossibly fast. If we succeed, it will likely 100 | render the above weak calculation superfluous. 101 | 102 | The following is rough: Consider what happens when a dihedral angle between two adjacent triangles is bounded 103 | away from both 0 and $pi$ (neither flat not completely folded). If we restrict to only considering similarly 104 | shaped hinges (which we can build into our volume measure), there can only be infinitely many stacked copies 105 | if they are nested. Thus, our path space is either locally sparse or locally very nested with a local total order. 106 | The problem is that this total order is unlikely to conveniently propagate around since adjacent hinges necessarily 107 | keep flipping. So this direction may go nowhere. 108 | 109 | More roughness: consider four close triangles $a,b,c,d$ in order from lowest to highest along their roughly 110 | shared normal $n$, and the geodesic paths $\gamma_{ac}$ and $\gamma_{db}$ both parameterized on $s \in [0,1]$. 111 | Consider the function 112 | $$h(s) = (n_{ac}(s) + n_{bd}(s)) \cdot \left(\gamma_{ac}(s) - \gamma_{bd}(s)\right)$$ 113 | where $n_{ac}(s)$ and $n_{bd}(s)$ are (roughly) the normals to $\gamma_{ac}$ and $\gamma_{bd}$. $h(s)$ can 114 | be made continuous if we add a delay at each edge crossing to slowly change normal. 115 | We have $h(0) < 0$, $h(1) > 0$, so there must be a zero crossing $h(s^*) = 0$. What is happening at this 116 | crossing? There are several (rough) options: 117 | \begin{enumerate} 118 | \item The two points lie in the same triangle of the same surface (or say within geodesic distance 2 on the surface). 119 | \item The two points are in different ``simplex classes'': triangle interior, edge interior, vertex. 120 | \item The two points are both in the interior of their triangles, and are far apart (necessary to cross). 121 | Note that they may have opposite normals (is this a problem?). 122 | \item Both points are on the interior of their edges, but are on different looking hinges. If the hinges 123 | are sufficiently similar, $h(s)$ can't change sign. 124 | \item Both points are very close to a vertex. I believe this still rules out a crossing. 125 | \end{enumerate} 126 | It seems like all but the first case should produce a nontrivial nonintersecting volume measure assigned to 127 | this crossing event. Actually I don't know whether these events can be arranged to be nonintersecting so 128 | as to produce a contradiction. In any case, we have another problem: if we have an exponential number of 129 | close triangles in $T_n$, is there at least an exponential number of pairs of pairs whose geodesics don't 130 | cross, and whose heights have the right ordering? 131 | 132 | \begin{comment} 133 | \begin{lemma} 134 | Let $P \subset B_r$ be a set of points separated by distance at least 1 in a hyperbolic disk of radius $r$ 135 | with $|P| = \Theta(\beta^r)$ for some $\beta > 1$. Totally order $P$. Then there exists $\kappa > 1$ 136 | and an ordered subset $p_1, \ldots, p_{2n} \in P$ with $n = \Theta(\kappa^r)$ s.t.\ the geodesics 137 | 138 | Let B(r) be a ball of radius r in the hyperbolic plane, which has 139 | volume Theta(A^r). Choose Theta(A^r/r^9) points x_i pairwise 140 | separated by at least distance 1, and total order them somehow: x_1 < 141 | x_2 < ... Does there exist an absolute constant B > 1 s.t. there 142 | always exists a totally ordered subsequence y_1 < ... < y_(2n) of size 143 | Theta(B^r) s.t. the n geodesics from y_i to y_(i+n) are pairwise 144 | separated by at least 1? 145 | 146 | Intuition: x_i are the centers of triangles that are very close 147 | together, and we want to arrive at a contradiction by analyzing how 148 | the various geodesics between x_i to x_j "change height order". If 149 | the above was true, we wouldn't have to worry about the geodesics 150 | dodging contradictions by simply sharing triangles. 151 | \end{comment} 152 | 153 | \end{document} 154 | -------------------------------------------------------------------------------- /poincare: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import division,print_function,unicode_literals 4 | from geode.value import parser 5 | from geode import * 6 | from fractal_helper import * 7 | from collections import defaultdict 8 | import sys 9 | import re 10 | 11 | # Depth 4: 12 | # ./poincare --mode flop --tolerance 1e-3 --depth 4 --separation .05 13 | 14 | # Properties 15 | props = PropManager() 16 | mode = props.add('mode','disk').set_allowed('disk flop test'.split()) 17 | depth = props.add('depth',3) 18 | resolution = props.add('resolution',20) 19 | tolerance = props.add('tolerance',1e-2) 20 | separation = props.add('separation',.8) 21 | levels = props.add('levels',1) 22 | degree = props.add('degree',7) 23 | poly = props.add('poly',3) 24 | autosave = props.add('autosave','') 25 | checkpoint = props.add('checkpoint',20) 26 | restart = props.add('restart','') 27 | method = props.add('method','CG').set_allowed('''jitter limit 28 | Nelder-Mead Powell CG BFGS Newton-CG Anneal L-BFGS-B TNC COBYLA SLSQP dogleg trust-ncg'''.split()) 29 | flopseed = props.add('flopseed',8231110) 30 | parser.parse(props,'Hyperbolic triangulation layout',) 31 | 32 | c2r_dtype = {dtype(complex64):dtype(float32),dtype(complex128):dtype(float64)} 33 | def splitcomplex(z): 34 | w = empty(z.shape+(2,),dtype=c2r_dtype[z.dtype]) 35 | mergecomplex(w)[...] = z 36 | return w 37 | 38 | r2c_dtype = {dtype(float32):dtype(complex64),dtype(float64):dtype(complex128)} 39 | def mergecomplex(z): 40 | z = asarray(z) 41 | assert z.shape[-1]==2 42 | return z.view(r2c_dtype[z.dtype]).reshape(z.shape[:-1]) 43 | 44 | def distance(u,v=zeros(2)): 45 | '''Measure distance in the Poincare disk model of the hyperbolic plane. 46 | See http://en.wikipedia.org/wiki/Poincare_disk for details.''' 47 | return acosh(1+2*sqr_magnitudes(u-v)/((1-sqr_magnitudes(u))*(1-sqr_magnitudes(v)))) 48 | 49 | class Mobius(ndarray): 50 | # Mobius group theory: 51 | # w = (az+b)/(cz+d) 52 | # When does f(z) preserve the unit circle? 53 | # http://math.stackexchange.com/questions/34071/mobius-transforms-that-preserve-the-unit-disk 54 | # f(z) = r (z+a)/(1+a'z) 55 | # g(z) = s (z+b)/(1+b'z) 56 | # f(g(z)) = r (s(z+b)/(1+b'z)+a) / (1 + a's(z+b)/(1+b'z)) 57 | # = r (s(z+b)+a(1+b'z)) / (1+b'z + a's(z+b)) 58 | # = r ((s+ab')z + sb+a) / (1+sa'b + (b'+sa')z) 59 | # = rs ((1+s'ab')z + b+s'a) / (1+sa'b + (b+s'a)'z) 60 | # Let t = (1+s'ab')/(1+sa'b), so that |t| = 1. Let c = (b+s'a)/(1+s'ab'). We have 61 | # f(g(z)) = rst (z+c)/(1+c'z) 62 | 63 | __array_priority__ = -1. 64 | 65 | def __array_finalize__(self,*args): 66 | '''View the 2x2 complex matrices m as Mobius transforms. 67 | With no arguments, return a single identity transform.''' 68 | assert iscomplexobj(self) 69 | assert self.shape[-2:]==(2,2) 70 | 71 | def __mul__(self,z): 72 | if isinstance(z,Mobius): 73 | return multiply(self.view(ndarray)[...,None],z.reshape(z.shape[:-2]+(1,2,2))).sum(axis=-2).view(Mobius) 74 | else: 75 | z = mergecomplex(z) 76 | a,b,c,d = rollaxis(self.view(ndarray).reshape(self.shape[:-2]+(4,)),-1) 77 | return splitcomplex((a*z+b)/(c*z+d)) 78 | 79 | def __pow__(self,e): 80 | if isinstance(e,int): 81 | if e == 0: 82 | return Mobius.identity() 83 | else: 84 | return self*(self**(e-1)) 85 | else: 86 | raise NotImplemented(type(e)) 87 | 88 | def inverse(self): 89 | a,b,c,d = rollaxis(self.view(ndarray).reshape(self.shape[:-2]+(4,)),-1) 90 | i = empty(self.shape,self.dtype) 91 | i[...,0,0] = d 92 | i[...,0,1] = -b 93 | i[...,1,0] = -c 94 | i[...,1,1] = a 95 | return (i/(a*d-b*c)[...,None,None]).view(Mobius) 96 | 97 | def normalized(self): 98 | a,b,c,d = rollaxis(self.view(ndarray).reshape(self.shape[:-2]+(4,)),-1) 99 | return self/sqrt(a*d-b*c)[...,None,None] 100 | 101 | @staticmethod 102 | def identity(dtype=complex128): 103 | return eye(2,dtype=dtype).view(Mobius) 104 | 105 | @staticmethod 106 | def from_angle(t): 107 | t = asarray(t) 108 | m = zeros(t.shape+(2,2,2),t.dtype) 109 | m[...,0,0,:] = polar(t) 110 | m[...,1,1,0] = 1 111 | return mergecomplex(m).view(Mobius) 112 | 113 | @staticmethod 114 | def translation(u): 115 | assert not iscomplexobj(u) 116 | "Walk the origin distance |u| in direction u/|u|" 117 | d,u = magnitudes_and_normalized(u) 118 | # Build rotations by u and conj(u) 119 | u = mergecomplex(u) 120 | # Here is how far the origin will go in the Poincare disk: 121 | # distance(v,0) = d 122 | # acosh(1+2v'v/(1-v'v)) = d 123 | # 1+2v'v/(1-v'v) = cosh(d) 124 | # v'v/(1-v'v) = (cosh(d)-1)/2 = b 125 | # v'v = b/(1+b) 126 | b = (cosh(d)-1)/2 127 | vv = b/(1+b) 128 | v = sqrt(vv) 129 | # If u is real, our Mobius transform will look like 130 | # f(z) = r (z+a)/(1+a'z) 131 | # f(1) = r (1+a)/(1+a') = 1 132 | # v = f(0) = r a 133 | # a = v 134 | # r = 1 135 | # f(z) = (z+v)/(vz+1) 136 | # If u is not real, we pre and post rotate: 137 | # f(z) = u(u'z+v)/(u'vz+1) = (z+uv)/(u'vz+1) 138 | # Assemble transform 139 | m = empty(d.shape+(2,2),dtype=u.dtype) 140 | m[...,0,0] = 1 141 | m[...,0,1] = u*v 142 | m[...,1,0] = conj(u)*v 143 | m[...,1,1] = 1 144 | return m.view(Mobius) 145 | 146 | def __str__(self): 147 | return str(self.view(ndarray)) 148 | 149 | def __repr__(self): 150 | return repr(self.view(ndarray)) 151 | 152 | @staticmethod 153 | def empty(shape,dtype=complex128): 154 | if not isinstance(shape,tuple): 155 | shape = shape, 156 | return empty(shape+(2,2),dtype).view(Mobius) 157 | 158 | @staticmethod 159 | def concat(*args): 160 | return concatenate([a.view(ndarray) for a in args]).view(Mobius) 161 | 162 | @staticmethod 163 | def close(x,y=None): 164 | y = Mobius.identity() if y is None else y.normalized() 165 | return allclose(x.normalized(),y) 166 | 167 | def test_mobius(): 168 | random.seed(821) 169 | # Rotations 170 | v = (.2,.2) 171 | theta = random.randn() 172 | r = Mobius.from_angle(theta) 173 | assert allclose(r*v,Rotation.from_angle(theta)*v) 174 | # Translations 175 | u = random.randn(2) 176 | t = Mobius.translation(u) 177 | tf = Mobius.translation(-u) 178 | assert allclose(distance(t*zeros(2)),magnitude(u)) 179 | assert allclose(distance(t*zeros(2),zeros(2)),magnitude(u)) 180 | assert allclose(distance(zeros(2),t*zeros(2)),magnitude(u)) 181 | assert allclose(angle_between(t*zeros(2),u),0) 182 | # Inverses 183 | m = Mobius.translation(u)*Mobius.translation(random.randn(2)) 184 | assert Mobius.close(m*m.inverse()) 185 | assert Mobius.close(m.inverse()*m) 186 | # Identities 187 | f = Mobius.from_angle(pi) 188 | assert Mobius.close(t*tf) 189 | assert Mobius.close(t*f,f*tf) 190 | assert Mobius.close(t*t,Mobius.translation(2*u)) 191 | 192 | @cache 193 | def equilateral_length(): 194 | t = 2*pi/degree() 195 | # The hyperbolic law of cosines for angle t, side s is 196 | # cos t = -cos^2 t + sin^2 t cosh a 197 | # a = acosh (cos(t)*(1+cos(t))/sin(t)^2) 198 | if poly()==3: 199 | c,s = cos(t),sin(t) 200 | return acosh(c*(1+c)/(s*s)) 201 | elif poly()==4: 202 | # cos(t/2) = -cos(t)*cos(t/2)+sin(t)*sin(t/2)*cosh(side) 203 | return acosh(cos(t/2)*(1+cos(t))/(sin(t)*sin(t/2))) 204 | else: 205 | raise NotImplemented(poly()) 206 | 207 | @cache 208 | def lattice(): 209 | '''Compute lattice points and transforms of the standard 7-valent triangulation 210 | of the Poincare disk model of the hyperbolic plane.''' 211 | a = 2*pi/degree() 212 | advance = Mobius.translation((equilateral_length(),0))*Mobius.from_angle(pi) 213 | rotate = Mobius.from_angle(a*arange(degree())) 214 | protate = advance*rotate[1] 215 | polygon = array([protate**i for i in xrange(1,poly()-1)])[:,None].view(Mobius) 216 | link = (rotate[::-1]*polygon*rotate).reshape(-1,2,2) 217 | levels = [Mobius.identity()[None,:]] 218 | for d in xrange(depth()): 219 | next = (link[:,None]*levels[-1]).reshape(-1,2,2) 220 | levels.append(next) 221 | trans = Mobius.concat(*levels) 222 | # Prune duplicates 223 | compact = ParticleTree(trans*zeros(2),1).remove_duplicates(1e-7) 224 | trans = trans[unique(compact,return_index=1)[1]] 225 | print('count = %d'%len(trans)) 226 | return trans 227 | 228 | def lattice_edges(lattice): 229 | a = 2*pi/degree() 230 | tree = ParticleTree(lattice*zeros(2),1) 231 | step = equilateral_length() 232 | walks = Mobius.translation(step*polar(a*arange(degree()))) 233 | edges = [] 234 | for i,m in enumerate(lattice): 235 | for walk in walks: 236 | p = m*walk*zeros(2) 237 | cp,j = tree.closest_point(p,1e-7) 238 | if all(isfinite(cp)) and distance(p,cp)<.1: 239 | edges.append((i,j)) 240 | return asarray(edges,dtype=int32) 241 | 242 | def interpolate(x,y,t): 243 | m = Mobius.translation(distance(x)[...,None]*normalized(x)) 244 | y = m.inverse()*y 245 | dy = distance(y)[...,None]*normalized(y) 246 | t = asarray(t)[...,None] 247 | return m*(Mobius.translation(t*dy)*zeros(2)) 248 | 249 | def find_quads(X,edges): 250 | near = defaultdict(lambda:set()) 251 | for x,y in edges: 252 | near[x].add(y) 253 | near[y].add(x) 254 | quads = set() 255 | for x,y in sort(edges,axis=1): 256 | for z in near[y]: 257 | if x 0: 261 | quads.add((x,y,z,w)) 262 | else: 263 | quads.add((x,w,z,y)) 264 | return asarray(sorted(quads),dtype=int32) 265 | 266 | @cache 267 | def pruned_lattice(): 268 | if poly()==3: 269 | return lattice() 270 | X = lattice()*zeros(2) 271 | edges = lattice_edges(lattice()) 272 | return lattice()[unique(find_quads(X,edges).ravel())] 273 | 274 | @cache 275 | def lattice_mesh(): 276 | X = pruned_lattice()*zeros(2) 277 | edges = lattice_edges(pruned_lattice()) 278 | if poly()==3: 279 | near = defaultdict(lambda:set()) 280 | for x,y in edges: 281 | near[x].add(y) 282 | near[y].add(x) 283 | tris = set() 284 | for x,y in edges: 285 | for z in near[x]: 286 | if z in near[y]: 287 | a,b,c = sorted((x,y,z)) 288 | if cross(X[b]-X[a],X[c]-X[a])<0: 289 | b,c = c,b 290 | tris.add((a,b,c)) 291 | return TriangleSoup(sorted(tris)) 292 | elif poly()==4: 293 | quads = find_quads(X,edges) 294 | return PolygonSoup(4*ones(len(quads),dtype=int32),quads.ravel()) 295 | else: 296 | raise NotImplemented(poly()) 297 | 298 | def subdivided(): 299 | mesh = lattice_mesh() 300 | X = pruned_lattice()*zeros(2) 301 | if levels()==1: 302 | return mesh,X 303 | elif levels()==2 or levels()==4: 304 | for i in xrange(int(rint(log2(levels())))): 305 | subdiv = TriangleSubdivision(mesh) 306 | i,j = mesh.segment_soup().elements.T 307 | X = concatenate([X,interpolate(X[i],X[j],1/2)]) 308 | mesh = subdiv.fine_mesh 309 | return mesh,X 310 | else: 311 | raise NotImplementedError('only subdivision levels 1,2,3 are allowed, got %d'%levels()) 312 | fine_lattice_mesh = cache(lambda:subdivided()[0]) 313 | fine_points = cache(lambda:subdivided()[1]) 314 | 315 | def jittermin(f,X,alpha=.99,beta=1.001,callback=None): 316 | random.randn(172131) 317 | step = 1. 318 | fx = f(X) 319 | while 1: 320 | X2 = X+step*random.randn(*X.shape) 321 | f2 = f(X2) 322 | if f2 < fx: 323 | step *= beta 324 | X = X2 325 | fx = f2 326 | print('SUCCESS: step %g'%step) 327 | if callback is not None: 328 | callback(X) 329 | else: 330 | step *= alpha 331 | print('FAIL: step %g'%step) 332 | 333 | @cache 334 | def flopsolve(): 335 | X0 = fine_points() 336 | n = len(X0) 337 | edges = fine_lattice_mesh().segment_soup().elements 338 | rest = distance(X0[edges[:,0]],X0[edges[:,1]]) 339 | rest /= rest.mean() 340 | if poly()==4: 341 | for count in fine_lattice_mesh().counts: 342 | assert count == 4 343 | diagonals = [fine_lattice_mesh().vertices[4*i:4*i+3:2] for i in xrange(len(fine_lattice_mesh().counts))] + [fine_lattice_mesh().vertices[4*i+1:4*i+4:2] for i in xrange(len(fine_lattice_mesh().counts))] 344 | edges = array(list(edges) + list(diagonals)) 345 | rest = array(list(rest) + [2.]*len(diagonals)) 346 | def lengths(X): 347 | return magnitudes(X[edges[:,1]]-X[edges[:,0]]) 348 | 349 | # Scale so that the average edge length is closer to 1 350 | X0 /= lengths(X0).mean() 351 | # Move to 3D 352 | X0 = hstack([X0,zeros(n)[:,None]]) 353 | # Apply some noise so that everything doesn't start in the plane 354 | random.seed(flopseed()) 355 | X0 += .1*random.randn(n,3) 356 | 357 | # Define problem 358 | close = separation() 359 | stiff = 10 360 | if poly() > 3: 361 | mesh = fine_lattice_mesh().triangle_mesh() 362 | collisions = SimpleCollisions(mesh,X0,close,True) 363 | else: 364 | collisions = SimpleCollisions(fine_lattice_mesh(),X0,close,True) 365 | def energy(X,strict=True): 366 | X = X.reshape(-1,3) 367 | if strict and collisions.collisions(X): 368 | return 1e6 369 | Le = lengths(X)-rest 370 | L = sqr_magnitude(Le)/2 371 | Lm = maxabs(Le/rest) 372 | C = stiff*collisions.energy(X) if stiff else 0 373 | print('energies: L = %g (max %g), C = %g'%(L,Lm,C)) 374 | return L+C 375 | def gradient(X): 376 | X = X.reshape(-1,3) 377 | L = unit_spring_energy_gradient(edges,rest,X) 378 | if stiff: 379 | C = stiff*collisions.gradient(X) 380 | return (L+C).ravel() 381 | else: 382 | return L.ravel() 383 | def worst(X): 384 | if collisions.collisions(X): 385 | C = inf 386 | else: 387 | C = close-collisions.closest(X) 388 | L = maxabs(lengths(X)/rest-1) 389 | print('worst: L = %g, C = %g'%(L,C)) 390 | return max(L,C) 391 | 392 | # Test 393 | dX = 1e-7*random.randn(n,3) 394 | X = random.randn(n,3) 395 | numerical = energy(X+dX,strict=0)-energy(X-dX,strict=0) 396 | analytic = 2*dot(gradient(X),dX.ravel()) 397 | error = relative_error(numerical,analytic) 398 | print('numerical = %g, analytic = %g, error = %g'%(numerical,analytic,error)) 399 | assert error<1e-4 400 | 401 | # Restart if requested 402 | if restart(): 403 | tm,X0 = read_mesh(restart()) 404 | assert all(tm.elements()==fine_lattice_mesh().elements) 405 | 406 | # Optionally write out checkpoints 407 | callback = None 408 | if checkpoint(): 409 | iterations = [0] 410 | m = re.match(r'^poincare-checkpoint-(\d+).obj$',restart()) 411 | if m: 412 | iterations[0] = int(m.group(1)) 413 | def checkwrite(X): 414 | iterations[0] += 1 415 | if iterations[0]%checkpoint()==0: 416 | name = 'poincare-checkpoint-%d.obj'%iterations[0] 417 | print('saving %s'%name) 418 | write_mesh(name,fine_lattice_mesh(),X.reshape(-1,3)) 419 | callback = checkwrite 420 | 421 | # Solve 422 | if method()=='jitter': 423 | jittermin(worst,X0,callback=callback) 424 | elif method()=='limit': 425 | X = X0 426 | wX = worst(X) 427 | alpha = .1 428 | print('worst start = %g'%worst(X)) 429 | while alpha > 1e-10: 430 | X2 = collisions.strain_limit(rest,X,alpha) 431 | w2 = worst(X2) 432 | if wX > w2 or isfinite(w2): 433 | X = X2 434 | wX = w2 435 | alpha *= 1 436 | else: 437 | alpha *= .9 438 | print('alpha = %g'%alpha) 439 | else: 440 | import scipy.optimize 441 | result = scipy.optimize.minimize(energy,X0.ravel(),jac=gradient,tol=tolerance(),callback=callback,method=method()) 442 | X = result.x.reshape(-1,3) 443 | print('energy = %g'%energy(X)) 444 | L = lengths(X) 445 | lo,hi = (L/rest).min(),(L/rest).max() 446 | print('length ratio range = %g %g (width %g)'%(lo,hi,hi-lo)) 447 | print('closest = %g'%collisions.closest(X)) 448 | print('collisions = %d'%collisions.collisions(X)) 449 | return X 450 | 451 | def flop(): 452 | def save_mesh(name='poincare.obj'): 453 | write_mesh(name,fine_lattice_mesh(),flopsolve()) 454 | if autosave(): 455 | save_mesh(autosave()) 456 | flopsolve() 457 | try: 458 | import gui 459 | app = gui.QEApp(sys.argv,True) 460 | main = gui.MainWindow(props) 461 | main.view.add_scene('mesh',gui.MeshScene(props,fine_lattice_mesh,flopsolve,(.2,.2,1),(0,1,0))) 462 | main.add_menu_item('File','Save',save_mesh,'') 463 | main.init() 464 | app.run() 465 | except ImportError: 466 | print('Warning: other/gui not found, falling back to matplotlib') 467 | X = flopsolve() 468 | tris = fine_lattice_mesh().elements 469 | # Rescale since matplotlib is broken 470 | X -= X.min(axis=0) 471 | X /= X.max() 472 | # Plot 473 | from mpl_toolkits.mplot3d import Axes3D 474 | from mpl_toolkits.mplot3d.art3d import Poly3DCollection 475 | import matplotlib.pyplot as plt 476 | Axes3D(plt.figure()).add_collection3d(Poly3DCollection(X[tris])) 477 | plt.show() 478 | 479 | def plot(): 480 | import pylab 481 | # Draw vertices 482 | points = fine_points() 483 | print('points %s'%(points.shape,)) 484 | pylab.plot(points[:,0],points[:,1],'o') 485 | # Draw edges 486 | edges = fine_lattice_mesh().segment_soup().elements 487 | mesh = fine_lattice_mesh() 488 | if isinstance(mesh,TriangleSoup): 489 | print('triangles = %d'%len(mesh.elements)) 490 | else: 491 | print('polys = %d'%len(mesh.counts)) 492 | t = linspace(0,1,num=resolution()) 493 | paths = interpolate(points[edges[:,0]].reshape(-1,1,2),points[edges[:,1]].reshape(-1,1,2),t) 494 | for path in paths: 495 | pylab.plot(path[:,0],path[:,1],'g') 496 | # Show 497 | pylab.axes().set_aspect('equal') 498 | pylab.show() 499 | 500 | if __name__=='__main__': 501 | test_mobius() 502 | if mode()=='disk': 503 | plot() 504 | elif mode()=='flop': 505 | flop() 506 | elif mode()!='test': 507 | raise NotImplementedError("unknown mode '%s'"%mode()) 508 | -------------------------------------------------------------------------------- /poincare-instance: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import division,print_function,unicode_literals 4 | from geode.value import parser 5 | from geode.geometry.platonic import * 6 | from geode import * 7 | import sys 8 | import re 9 | 10 | # Properties 11 | props = PropManager() 12 | side = props.add('side',1.).set_help('side length of original infinitesimal triangle') 13 | body_side = props.add('body_side',.8).set_help('side length of triangle body') 14 | hole_side = props.add('hole_side',0.).set_help('side length of hole') 15 | thickness = props.add('thickness',.1).set_help('triangle thickness') 16 | rod_radius = props.add('rod_radius',.1).set_help('radius of a rod') 17 | stop_radius = props.add('stop_radius',.2).set_help('radius of the ends of a rod') 18 | inner_radius = props.add('inner_radius',.15).set_help('inner barrel radius') 19 | outer_radius = props.add('outer_radius',.25).set_help('outer barrel radius') 20 | stop_width = props.add('stop_width',.1).set_help('width of the end of a rod') 21 | barrel_width = props.add('barrel_width',.4).set_help('width of a barrel') 22 | lo_count = props.add('lo_count',6).set_help('low resolution') 23 | hi_count = props.add('hi_count',17).set_help('high resolution') 24 | types = props.add('types','012').set_help('0 = none, 1 = rod, 2 = barrel') 25 | union = props.add('union',True).set_help('perform csg union') 26 | separated = props.add('separated',True).set_help('use entirely separated hinges') 27 | parser.parse(props,'Hinged triangle generator') 28 | 29 | @cache 30 | def counts(): 31 | return lo_count(),hi_count() 32 | 33 | @cache 34 | def body(): 35 | t = thickness()/2 36 | br = sqrt(1/3)*body_side() 37 | hr = sqrt(1/3)*hole_side() 38 | if not separated(): 39 | if hr: 40 | return surface_of_revolution(0,(0,0,1),(br,br,hr,hr),(-t,t,t,-t),3,periodic=1) 41 | else: 42 | return apply(Rotation.from_angle_axis(pi/3,(0,0,1)),variable_sor((br,br),(-t,t),counts=(3,3))) 43 | else: 44 | raise NotImplemented() 45 | 46 | def apply(f,(mesh,X)): 47 | return mesh,(f*X if isinstance(f,(Frames,Rotation.Rotations3d)) else f(X)) 48 | 49 | def connector(r1,n1,z): 50 | lo = lo_count() 51 | if lo&1: 52 | a = 2*pi/lo/2 53 | else: 54 | a = 2*pi/lo 55 | xm = side()/tan(pi/3)-r1 56 | z0 = r1*sin(a) 57 | x0 = side()/tan(pi/3)-r1*cos(a) 58 | z1 = thickness()/2 59 | x1 = body_side()/2/tan(pi/3) 60 | if lo&1: 61 | flat = ((x1,z1),(x0,z0),(x0,-z0),(x1,-z1)) 62 | else: 63 | flat = ((x1,z1),(x0,z0),(xm,0),(x0,-z0),(x1,-z1)) 64 | mesh = MutableTriangleTopology() 65 | mesh.add_vertices(len(flat)) 66 | mesh.add_faces([(0,i+1,i) for i in xrange(1,len(flat)-1)]) 67 | return apply(Rotation.from_rotated_vector((0,0,1),(0,1,0)),extrude((mesh,flat),z)) 68 | 69 | @cache 70 | def rod(): 71 | x = sqrt(1/3)*side() 72 | r = Rotation.from_rotated_vector((0,0,1),(0,1,0)) 73 | f = Frames((x,0,0),r) 74 | r0 = rod_radius() 75 | r1 = stop_radius() 76 | w = body_side() 77 | sw = stop_width() 78 | lo,hi = counts() 79 | rod = apply(f,variable_sor([r1,r1,r0,r0,r1,r1],array([0,sw,sw,w-sw,w-sw,w])-w/2,[lo,lo,hi,hi,lo,lo])) 80 | return [rod,connector(r1,lo,(-w/2,sw-w/2)),connector(r1,lo,(w/2-sw,w/2))] 81 | 82 | @cache 83 | def barrel(): 84 | s = side() 85 | x = sqrt(1/3)*s 86 | r = Rotation.from_rotated_vector((0,0,1),(0,1,0)) 87 | f = Frames((x,0,0),r) 88 | bw = barrel_width() 89 | r0 = inner_radius() 90 | r1 = outer_radius() 91 | lo,hi = counts() 92 | barrel = apply(f,variable_sor([r0,r1,r1,r0],bw/2*asarray([-1,-1,1,1]),[hi,lo,lo,hi],periodic=1)) 93 | return [barrel,connector(r1,lo,(-bw/2,bw/2))] 94 | 95 | @cache 96 | def meshes(): 97 | rs = Rotation.from_angle_axis(2*pi/3*arange(3),(0,0,1)) 98 | kinds = {'0':[],'1':rod(),'2':barrel()} 99 | meshes = [body()]+[apply(rs[i],m) for i in xrange(3) for m in kinds[types()[i]]] 100 | for mesh,X in meshes: 101 | assert isinstance(mesh,TriangleSoup) 102 | assert isinstance(X,ndarray) 103 | print('types = %s'%types()) 104 | return meshes 105 | 106 | @cache 107 | def merged(): 108 | offset = 0 109 | tris = [] 110 | X = [] 111 | for m,x in meshes(): 112 | tris.append(m.elements+offset) 113 | X.append(x) 114 | offset += len(x) 115 | m,x = TriangleSoup(concatenate(tris)),concatenate(X) 116 | print('raw vertices = %d'%len(x)) 117 | print('raw faces = %d'%len(m.elements)) 118 | if union(): 119 | import tim.fab.geom 120 | from tim.otherfab import csg_union 121 | from geode.openmesh import decimate 122 | tm = TriMesh() 123 | tm.add_vertices(x) 124 | tm.add_faces(m.elements) 125 | tim.fab.geom.offset_mesh(tm,1e-6) 126 | tm = csg_union([tm]) 127 | decimate(tm,max_quadric_error=1e-3) 128 | m = TriangleSoup(tm.elements()) 129 | x = tm.X() 130 | print('vertices = %d\nfaces = %d'%(len(x),len(m.elements))) 131 | return m,x 132 | 133 | def main(): 134 | def save_mesh(name='poincare-instance.obj'): 135 | write_mesh(name,*merged()) 136 | try: 137 | import gui 138 | app = gui.QEApp(sys.argv,True) 139 | main = gui.MainWindow(props) 140 | main.view.add_scene('mesh', 141 | gui.MeshScene(props,cache(lambda:merged()[0]),cache(lambda:merged()[1]),(.2,.2,1),(0,1,0))) 142 | main.add_menu_item('File','Save',save_mesh,'') 143 | main.init() 144 | main.view.show_all(1) 145 | app.run() 146 | except ImportError: 147 | print('Warning: other/gui not found, falling back to matplotlib') 148 | X = merged()[1] 149 | tris = merged()[0].elements 150 | # Rescale since matplotlib is broken 151 | X -= X.min(axis=0) 152 | X /= X.max() 153 | # Plot 154 | from mpl_toolkits.mplot3d import Axes3D 155 | from mpl_toolkits.mplot3d.art3d import Poly3DCollection 156 | import matplotlib.pyplot as plt 157 | Axes3D(plt.figure()).add_collection3d(Poly3DCollection(X[tris])) 158 | plt.show() 159 | 160 | if __name__=='__main__': 161 | main() 162 | -------------------------------------------------------------------------------- /poincare-instance-sep: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import division,print_function,unicode_literals 4 | from geode.value import parser 5 | from geode.geometry.platonic import * 6 | from fractal_helper import * 7 | from tim.cgal import delaunay_polygon 8 | from geode import * 9 | import sys 10 | import re 11 | 12 | # Properties, with length units in mm. 13 | props = PropManager() 14 | side = props.add('side',18.5).set_help('side length of original infinitesimal triangle') 15 | thickness = props.add('thickness',.7).set_help('minimum wall thickness') 16 | separation = props.add('separation',.5).set_help('minimum separation') 17 | max_angle = props.add('max_angle',170.).set_help('maximum dihedral angle in degrees') 18 | count = props.add('count',17).set_help('resolution of the barrel') 19 | barrel_width = props.add('barrel_width',4.).set_help('width of a barrel') 20 | side_cut = props.add('side_trim',4.).set_help('cut the corners back by the given length') 21 | parser.parse(props,'Hinged triangle generator') 22 | 23 | def solve(f,lo,hi): 24 | x = scipy.optimize.brentq(f,lo,hi) 25 | assert allclose(f(x),0) 26 | return x 27 | 28 | @cache 29 | def body_side(): 30 | # The body triangle can't have edge length side(), since it has finite thickness 31 | # and is adjacent to another triangle. The two triangles need to be able to 32 | # rotate by max_angle(). 33 | cs = sqrt(3)/6*side() # Center to side distance 34 | t = thickness() 35 | s = separation() 36 | a = max_angle()/2 37 | # Erode center to side distance to make room for hing 38 | erode = (s/2+t/2*cos(a))/sin(a) 39 | bcs = cs-erode 40 | return 6/sqrt(3)*bcs 41 | 42 | @cache 43 | def barrel_inner_radius(): 44 | # The inner radius must be large enough to clear a width = thickness band of triangle material 45 | cs = sqrt(3)/6*side() 46 | bcs = sqrt(3)/6*body_side() 47 | t = thickness() 48 | s = separation() 49 | return separation()+magnitude((cs-bcs+t,t/2)) 50 | 51 | @cache 52 | def barrel_outer_radius(): 53 | return barrel_inner_radius()+thickness() 54 | 55 | @cache 56 | def body(): 57 | br = sqrt(1/3)*body_side() 58 | cs = sqrt(3)/6*side() 59 | bcs = sqrt(3)/6*body_side() 60 | t = thickness() 61 | s = separation() 62 | aa = 2*pi/3*arange(3) 63 | hx = bcs-t,cs-barrel_outer_radius()-s 64 | hy = barrel_width()/2+s 65 | hole = asarray([(hx[0],hy),(hx[0],-hy),(hx[1],-hy),(hx[1],hy)]) 66 | if not side_cut(): 67 | polys = [br*polar(aa+pi/3)] 68 | else: 69 | c = side_cut() 70 | y = body_side()/2-side_cut() 71 | polys = [(Rotation.from_angle(aa).reshape(-1,1)*[(bcs,-y),(bcs,y)]).reshape(-1,2)] 72 | print(polys[0]) 73 | print(hole) 74 | for a in aa: 75 | polys.append(Rotation.from_angle(a)*hole) 76 | # Mesh polygons 77 | import tim.cgal 78 | soup,X,_ = tim.cgal.delaunay_polygon(polys,0,0,False) 79 | mesh = MutableTriangleTopology() 80 | mesh.add_vertices(len(X)) 81 | mesh.add_faces(soup.elements) 82 | return extrude((mesh,X),(-t/2,t/2)) 83 | 84 | def apply(f,(mesh,X)): 85 | return mesh,(f*X if isinstance(f,(Frames,Rotation.Rotations3d)) else f(X)) 86 | 87 | @cache 88 | def barrel(): 89 | cx = sqrt(3)/6*side() 90 | r0 = barrel_inner_radius() 91 | r1 = barrel_outer_radius() 92 | w = barrel_width()/2 93 | return surface_of_revolution((cx,0,0),(0,1,0),resolution=count(), 94 | radius=(r0,r1,r1,r0),height=(-w,-w,w,w),periodic=True) 95 | 96 | @cache 97 | def merged(): 98 | tris = [] 99 | X = [] 100 | offset = 0 101 | for m,x in body(),barrel(): 102 | tris.append(m.elements+offset) 103 | X.append(x) 104 | offset += len(x) 105 | return TriangleSoup(concatenate(tris)),concatenate(X) 106 | 107 | def main(): 108 | def save_mesh(name='poincare-instance.obj'): 109 | write_mesh(name,*merged()) 110 | try: 111 | import gui 112 | app = gui.QEApp(sys.argv,True) 113 | main = gui.MainWindow(props) 114 | def add_mesh(name,mesh): 115 | def m(): 116 | m,x = mesh() 117 | print('%s: vertices %d, faces %d'%(name,len(x),len(m.elements))) 118 | return m 119 | main.view.add_scene(name,gui.MeshScene(props,cache(m),cache(lambda:mesh()[1]),(.2,.2,1),(0,1,0))) 120 | add_mesh('body',body) 121 | add_mesh('barrel',barrel) 122 | main.add_menu_item('File','Save',save_mesh,'') 123 | main.init() 124 | main.view.show_all(1) 125 | app.run() 126 | except ImportError: 127 | print('Warning: other/gui not found, falling back to matplotlib') 128 | X = merged()[1] 129 | tris = merged()[0].elements 130 | # Rescale since matplotlib is broken 131 | X -= X.min(axis=0) 132 | X /= X.max() 133 | # Plot 134 | from mpl_toolkits.mplot3d import Axes3D 135 | from mpl_toolkits.mplot3d.art3d import Poly3DCollection 136 | import matplotlib.pyplot as plt 137 | Axes3D(plt.figure()).add_collection3d(Poly3DCollection(X[tris])) 138 | plt.show() 139 | 140 | if __name__=='__main__': 141 | main() 142 | -------------------------------------------------------------------------------- /poincare-laser: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import division,print_function 4 | from geode import * 5 | import pylab 6 | from matplotlib.patches import Polygon 7 | 8 | s = polar(2*pi/6*arange(6)) 9 | d = asarray([s[0],s[-1],s[1],s[2],s[2],s[3],s[-1],s[-2]]) 10 | p = concatenate([[zeros(2)],cumsum(d,axis=0)]) 11 | assert allclose(p[0],p[-1]) 12 | p = p[:-1] 13 | 14 | if 0: 15 | ax = pylab.axes() 16 | for i in arange(-10,10): 17 | for j in arange(-10,10): 18 | ax.add_patch(Polygon(p+i*(s[0]+s[1])+j*3*s[2],closed=1,fill='r')) 19 | ax.set_aspect('equal') 20 | pylab.xlim(-10,10) 21 | pylab.ylim(-10,10) 22 | pylab.show() 23 | 24 | if 1: 25 | z = zeros(2) 26 | def line(a,b): 27 | pylab.plot([a[0],b[0]],[a[1],b[1]]) 28 | line(z,2*s[0]) 29 | line(z,2*s[1]) 30 | line(2*s[0],2*s[1]) 31 | line(s[0]+s[-1],s[1]+s[2]) 32 | line(s[0],s[0]+s[1]) 33 | line(s[1],s[0]+s[1]) 34 | line(2*s[0],2*s[0]-s[1]) 35 | line(2*s[1],2*s[1]-s[0]) 36 | pylab.axes().set_aspect('equal') 37 | pylab.show() 38 | -------------------------------------------------------------------------------- /poker: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import division 4 | from numpy import * 5 | import pylab 6 | 7 | def limit_donkey_equity(k): 8 | """In the case of infinite stack size, Alice's optimal strategy is simply to wait for AA. The resulting 9 | win probability function satisfies 10 | p[t] = 0, t <= 0 11 | p[t] = 1, t >= 1 12 | p[t] = a p[2t] + (1-a) p[2t-1], 0 < t < 1 13 | where a is the probability of winning with AA (splitting ties). Important properties: 14 | 1. p is continuous. 15 | 2. If t is a dyadic rational, p[t] is a function of simpler dyadic rationals. 16 | This provides an easy, superfast way of computing p. 17 | 18 | Bug: actually, splitting ties doesn't produce the right answer. To be correct, "a" below should 19 | become b/(1-c), where a is the win probability and c is the tie probability. Still a fractal, though. 20 | """ 21 | a = 893604787/1048786200 22 | p = zeros(1) 23 | for k in xrange(k): 24 | p = hstack([a*p,a+(1-a)*p]) 25 | return hstack([p,1]) 26 | 27 | k = 20 28 | t = arange(2**k+1)/2**k 29 | p = limit_donkey_equity(k) 30 | 31 | pylab.plot(t,p) 32 | pylab.show() 33 | -------------------------------------------------------------------------------- /render-dragon: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import division 4 | import sys 5 | import subprocess 6 | import os.path 7 | from geode import * 8 | from geode.value import parser 9 | from mitsuba import * 10 | from fractal_helper import * 11 | 12 | # Fine front: 13 | # ./dragon.py --type dragon --level 17 --smooth 5 --corner-shift 0.05 --size 150 --thickness 0.025 --closed 0 --closed-base 1 --z-scale 0.5 --sharp-corners 0 --colorize 1 --instance 1 --border-crease 1 --ground 1 --settle-step 0.01 --mitsuba-dir gen-fine --origin=-49.412825379531753,208.92850880962186,78.324132111435546 --target 1.8198107879612146,-9.5738105894434717,53.519032449873805 --rotation 0,-0.9999999984957032,5.4850649339629963e-05,0 --border-layers 8 14 | # ./render-dragon --gui 1 --samples 512 --data gen-fine --width 1280 --height 960 --color-seed 194514 15 | 16 | # Gold 17 | # ./dragon.py --type dragon --level 17 --smooth 5 --corner-shift 0.05 --size 150 --thickness 0.025 --closed 0 --closed-base 1 --z-scale 0.5 --sharp-corners 0 --colorize 1 --instance 1 --border-crease 1 --ground 1 --settle-step 0.01 --mitsuba-dir gen-gold --origin=-49.412825379531753,208.92850880962186,78.324132111435546 --target 1.8198107879612146,-9.5738105894434717,53.519032449873805 --rotation 0,-0.9999999984957032,5.4850649339629963e-05,0 --border-layers 8 --two-ring 1 18 | # ./render-dragon --gui 1 --samples 512 --data gen-gold --width 1280 --height 960 --multicolor 0 --integrator erpt --depth 20 19 | 20 | # Simple: 21 | # for i in `seq 0 5`; do ./dragon.py --console 1 --smooth $i --mitsuba-dir gen-simple-$i --type dragon --level 5 --corner-shift 0.05 --size 150 --thickness 0.1 --closed 0 --closed-base 1 --z-scale 0.5 --sharp-corners 0 --colorize 0 --instance 1 --border-crease 1 --border-layers 1 --flip 1 --ground 1 --settle-step 0.01 --origin=-6.6614485991588577,214.62817962567379,78.913429143861407 --target 1.8198107879612146,-9.5738105894434717,53.519032449873805 --rotation 0,-0.9999999984957032,5.4850649339629963e-05,0; done 22 | # for i in `seq 0 5`; do ./render-dragon --data gen-simple-$i --samples 512 --simple-color .8,0,0 --facets 1 -o dragon-simple-$i.exr; done 23 | 24 | # Fine down/side: 25 | # ./dragon.py --type dragon --level 13 --smooth 5 --corner-shift 0.05 --size 150 --thickness 0.2 --closed 0 --closed-base 1 --z-scale 0.5 --sharp-corners 0 --colorize 1 --instance 1 --border-crease 1 --ground 1 --settle-step 0.01 --mitsuba-dir gen-side-fine --origin=-39.679084283641487,-239.85430391769961,75.543129644474547 --target 5.2339400073202738,-10.984723621310748,11.128750338448013 --rotation=-0.09978530235048004,0.080263531268720989,0.62102958705347577,-0.77325475167456148 26 | # ./render-dragon --gui 1 --view down --data gen-side-fine --width 1280 --height 960 --color-seed 194514 --samples 512 & 27 | # ./render-dragon --gui 1 --view side --data gen-side-fine --width 1280 --height 960 --color-seed 194514 --samples 512 & 28 | 29 | # Canopy: 30 | # ./dragon.py --type dragon --level 14 --scale-level 14 --smooth 2 --corner-shift 0.05 --size 150 --thickness 0.17999999999999999 --closed 0 --closed-base 1 --z-scale 0.5 --sharp-corners 0 --colorize 0 --instance 1 --border-crease 1 --flip 1 --ground 0 --settle-step 0.01 --mitsuba-dir gen-canopy --origin 15.937630749436821,-10.55833672257492,60.718947647935565 --target 18.266964477314549,-10.578622900392148,71.818862427738665 --rotation 1,0,0,0 31 | # ./render-dragon --view canopy --data gen-canopy-fine --samples 512 --width 1280 --height 960 --gui 1 32 | 33 | # Fine canopy 34 | # ./dragon.py --type dragon --level 14 --scale-level 14 --smooth 5 --corner-shift 0.05 --size 150 --thickness 0.17999999999999999 --closed 0 --closed-base 1 --z-scale 0.5 --sharp-corners 0 --colorize 0 --instance 1 --border-crease 1 --flip 1 --ground 0 --settle-step 0.01 --mitsuba-dir gen-canopy-fine --origin 15.937630749436821,-10.55833672257492,60.718947647935565 --target 18.266964477314549,-10.578622900392148,71.818862427738665 --rotation 1,0,0,0 35 | 36 | # Fine terdragon front: 37 | # ./dragon.py --type terdragon --level 10 --smooth 5 --colorize 1 --ground 1 --mitsuba-dir gen-terdragon-front --origin=-49.412825379531753,208.92850880962186,78.324132111435546 --target 1.8198107879612146,-9.5738105894434717,53.519032449873805 --rotation 1.7855807963205805e-11,1,0,0 --thickness .05 --border-layers 8 38 | # ./render-dragon --gui 1 --view front --data gen-terdragon-front --samples 512 --width 1280 --height 960 --color-seed 20002 39 | 40 | # Fine terdragon side/down: 41 | # ./dragon.py --type terdragon --level 8 --smooth 5 --thickness 0.29999999999999999 --colorize 1 --border-layers 8 --ground 1 --mitsuba-dir gen-terdragon-side --origin=-70.331627112440813,164.23717989126743,65.999483565575716 --target 0.57691794482902314,-7.4761573866077597,26.788706998314769 --rotation=-0.74593201261051922,0.62282714395871686,0.13974285587294705,-0.19011500609767951 42 | # ./render-dragon --view terdragon-side --data gen-terdragon-side --samples 512 --width 1280 --height 960 --color-seed 20002 43 | # ./render-dragon --view terdragon-down --data gen-terdragon-side --samples 512 --width 1280 --height 960 --color-seed 20002 44 | 45 | # Koch: 46 | # ./dragon.py --type koch --level 6 --smooth 5 --colorize 1 --border-layers 8 --ground 1 --mitsuba-dir gen-koch --origin=-46.082316229740172,179.20323643974311,73.74081360141966 --target=-0.47139844334676173,-7.465299519683386,26.072048830404896 --rotation=-1.7884692144788661e-08,0.95116086597988436,0.30869565437238849,-2.1036733629381521e-08 --thickness .01 47 | # ./render-dragon --view koch-front --data gen-koch --samples 512 --color-seed 184853 --width 1280 --height 960 -o koch-front.exr 48 | 49 | # Koch side: 50 | # ./dragon.py --type koch --level 6 --smooth 5 --colorize 1 --border-layers 8 --ground 1 --mitsuba-dir gen-koch-side --origin 0.22170089213766708,123.93437270853076,180.33272525228978 --target=-6.5876712447798162,-17.224156513626859,44.43756566063562 --rotation=1,0,0,0 --thickness .01 51 | # ./render-dragon --view koch-side --data gen-koch-side --samples 512 --color-seed 184853 --width 1280 --height 960 -o koch-side.exr 52 | 53 | # Gosper front: 54 | # ./dragon.py --type gosper --level 5 --scale-level 4 --smooth 4 --corner-shift 0.01 --thickness 0.3 --colorize 1 --border-layers 8 --ground 1 --origin=-37.548031375625783,174.33524984605464,94.06447355517362 --target 2.9977420980846889,-0.88814677571724876,21.815315250732041 --rotation=-5.4497675157361205e-09,-0.61958769628079802,0.78492744035194328,-5.7771780692661481e-09 --mitsuba-dir gen-gosper-front --console 1 55 | # ./render-dragon --view gosper-front --data gen-gosper-front --samples 512 --color-seed 184811 --width 1280 --height 960 56 | # ./render-dragon --view gosper-other --data gen-gosper-front --samples 512 --color-seed 184811 --width 1280 --height 960 57 | 58 | # Gosper back: 59 | # ./dragon.py --type gosper --level 5 --scale-level 4 --smooth 4 --corner-shift 0.01 --thickness 0.3 --colorize 1 --border-layers 8 --ground 1 --origin=-37.548031375625783,174.33524984605464,94.06447355517362 --target 2.9977420980846889,-0.88814677571724876,21.815315250732041 --rotation 4.9634164601974635e-09,-0.9374474864151201,-0.3481267157429479,-1.0434993813479565e-08 --mitsuba-dir gen-gosper-back --console 1 60 | # ./render-dragon --view gosper-back --data gen-gosper-back --samples 512 --color-seed 184811 --width 1280 --height 960 61 | 62 | # Gosper side: 63 | # ./dragon.py --type gosper --level 5 --scale-level 4 --smooth 4 --corner-shift 0.01 --thickness 0.29999999999999999 --colorize 1 --border-layers 8 --ground 1 --mitsuba-dir gen-gosper-side --origin 106.14852758526946,184.76928051872716,86.739813112730673 --target=-1.8413109040436795,8.757791108450121,17.952745587243811 --rotation 0.88225156371353819,-0.40498582943483014,0.019493838923558819,0.23924599584114714 64 | # ./render-dragon --view gosper-side --data gen-gosper-side --samples 512 --color-seed 184811 --width 1280 --height 960 65 | 66 | # Dragon for comparison 67 | # ./dragon.py --type dragon --level 10 --smooth 5 --corner-shift 0.05 --size 150 --thickness 0 --closed 0 --closed-base 1 --z-scale 0.56995792501637821 --sharp-corners 1 --colorize 0 --instance 0 --border-crease 1 --ground 1 --settle-step 0.01 --origin=-49.412825379531753,208.92850880962186,78.324132111435546 --target 1.8198107879612146,-9.5738105894434717,53.519032449873805 --rotation 0,-0.9999999984957032,5.4850649339629963e-05,0 --border-layers 8 --extra-mesh henry-thin-level11.stl --mitsuba-dir gen-dragon-compare-thin-sharp 68 | # ./dragon.py --type dragon --level 10 --smooth 5 --corner-shift 0.05 --size 150 --thickness 0 --closed 0 --closed-base 1 --z-scale 0.56995792501637821 --sharp-corners 0 --colorize 0 --instance 0 --border-crease 1 --ground 1 --settle-step 0.01 --origin=-49.412825379531753,208.92850880962186,78.324132111435546 --target 1.8198107879612146,-9.5738105894434717,53.519032449873805 --rotation 0,-0.9999999984957032,5.4850649339629963e-05,0 --border-layers 8 --extra-mesh henry-thin-level11.stl --mitsuba-dir gen-dragon-compare-thin-round 69 | # ./dragon.py --type dragon --level 10 --smooth 5 --corner-shift 0.05 --size 150 --thickness 0.2 --closed 0 --closed-base 1 --z-scale 0.56995792501637821 --sharp-corners 0 --colorize 0 --instance 0 --border-crease 1 --ground 1 --settle-step 0.01 --origin=-49.412825379531753,208.92850880962186,78.324132111435546 --target 1.8198107879612146,-9.5738105894434717,53.519032449873805 --rotation 0,-0.9999999984957032,5.4850649339629963e-05,0 --border-layers 8 --mitsuba-dir gen-dragon-compare-exp-round 70 | # ./dragon.py --type dragon --level 10 --smooth 5 --corner-shift 0.05 --size 150 --thickness 0.7 --thickness-alpha 1 --closed 0 --closed-base 1 --z-scale 0.56995792501637821 --sharp-corners 0 --colorize 0 --instance 0 --border-crease 1 --ground 1 --settle-step 0.01 --origin=-49.412825379531753,208.92850880962186,78.324132111435546 --target 1.8198107879612146,-9.5738105894434717,53.519032449873805 --rotation 0,-0.9999999984957032,5.4850649339629963e-05,0 --border-layers 8 --mitsuba-dir gen-dragon-compare-const-round 71 | # ./render-dragon --gui 1 --samples 512 --data gen-dragon-compare-thin-sharp --width 1280 --height 960 --two-sided 1 --simple-color .8,0,0 --extra 0 72 | # ./render-dragon --gui 1 --samples 512 --data gen-dragon-compare-thin-round --width 1280 --height 960 --two-sided 1 --simple-color .8,0,0 --extra 0 73 | # ./render-dragon --gui 1 --samples 512 --data gen-dragon-compare-exp-round --width 1280 --height 960 --two-sided 0 --simple-color .8,0,0 --extra 0 74 | # ./render-dragon --gui 1 --samples 512 --data gen-dragon-compare-const-round --width 1280 --height 960 --two-sided 0 --simple-color .8,0,0 --extra 0 75 | # ./render-dragon --gui 1 --samples 512 --data gen-dragon-compare-const-round --width 1280 --height 960 --two-sided 1 --simple-color .8,0,0 --extra 1 76 | 77 | # Sierpinski thickness comparison 78 | # ./dragon.py --type sierpinski --level 8 --smooth 4 --thickness 0.075 --thickness-alpha 1 --z-scale 0.6 --instance 0 --border-layers 8 --ground 1 --min-dot-override=-8.32499 --mitsuba-dir gen-sierpinski-const --origin 49.790814279239328,37.745462112076865,110.31450473396978 --target 46.830878243261445,33.319459198501335,103.8336842821352 --rotation 0.88684068551546047,-0.45487067454112651,-0.081278951500180857,0 --console 1 79 | # ./dragon.py --type sierpinski --level 8 --smooth 4 --thickness 0.075 --thickness-alpha -1 --z-scale 0.6 --instance 0 --border-layers 8 --ground 1 --min-dot-override=-8.32499 --mitsuba-dir gen-sierpinski-exp --origin 49.790814279239328,37.745462112076865,110.31450473396978 --target 46.830878243261445,33.319459198501335,103.8336842821352 --rotation 0.88684068551546047,-0.45487067454112651,-0.081278951500180857,0 --console 1 80 | # ./dragon.py --type sierpinski --level 8 --smooth 4 --thickness 0 --thickness-alpha -1 --z-scale 0.6 --instance 0 --border-layers 8 --ground 1 --min-dot-override=-8.32499 --mitsuba-dir gen-sierpinski-thin --origin 49.790814279239328,37.745462112076865,110.31450473396978 --target 46.830878243261445,33.319459198501335,103.8336842821352 --rotation 0.88684068551546047,-0.45487067454112651,-0.081278951500180857,0 --console 1 81 | # ./render-dragon --gui 1 --samples 512 --data gen-sierpinski-const --width 1280 --height 960 --two-sided 0 --simple-color .8,0,0 --view sierpinski-thick 82 | # ./render-dragon --gui 1 --samples 512 --data gen-sierpinski-exp --width 1280 --height 960 --two-sided 0 --simple-color .8,0,0 --view sierpinski-thick 83 | # ./render-dragon --gui 1 --samples 512 --data gen-sierpinski-thin --width 1280 --height 960 --two-sided 1 --simple-color .8,0,0 --view sierpinski-thick 84 | 85 | # Define properties 86 | props = PropManager() 87 | sun_direction = props.add('sun_direction',(.1,.1,1)) 88 | width = props.add('width',640).set_help('width of the rendered image') 89 | height = props.add('height',480).set_help('height of the rendered image') 90 | view = props.add('view','front').set_allowed('front down side canopy terdragon-side terdragon-down koch-front koch-side gosper-front gosper-back gosper-side gosper-other sierpinski-thick custom'.split()) 91 | origin = props.add('origin','160.64605078933045,3.4348049364373598,-16.681267849938735').set_help('custom view') 92 | target = props.add('target','154.06255051957712,3.1859855253969869,-16.190753540914127').set_help('custom view') 93 | norender = props.add('norender',False).set_abbrev('n') 94 | samples = props.add('samples',128).set_help('# of samples/pixel, render time is about linear in this') 95 | skyres = props.add('sky_resolution',512).set_help('resolution of sky environment map') 96 | output = props.add('output','').set_abbrev('o') 97 | multicolor = props.add('multicolor',True).set_help('use different colors for each patch') 98 | simple_color = props.add('simple_color',zeros(3)).set_help('use the given color for all patches') 99 | color_seed = props.add('color_seed',1618).set_help('random seed used for patch color generation') 100 | options = props.add('mitsuba','').set_help('options to pass to Mitsuba') 101 | gui = props.add('gui',False).set_help('run mtsgui') 102 | data = props.add('data','gen').set_help('data directory') 103 | facets = props.add('facets',False).set_help('use triangle normals') 104 | depth = props.add('depth',10).set_help('maximum reflection depth') 105 | two_stage = props.add('two_stage',False).set_help('use two stage pssmlt') 106 | integrator_type = props.add('integrator','path').set_allowed('pssmlt erpt path direct'.split()) 107 | sun_only = props.add('sun_only',False) 108 | sphere_sun = props.add('sphere_sun',False) 109 | sun_scale = props.add('sun_scale',0.) 110 | tile_size = props.add('tile_size',1000).set_help('base size of a tile in tiled mode') 111 | tile_overlap = props.add('tile_overlap',10).set_help('half overlap of adjacent tiles') 112 | tile_id = props.add('tile',-1).set_help('which tile to render') 113 | mask = props.add('mask','').set_help('render only pixels specified by the mask') 114 | two_sided = props.add('two_sided',False).set_help('render both sides of the surface') 115 | extra = props.add('extra',False).set_help('render extra mesh') 116 | parser.parse(props,'Render a developed fractal curve.',positional=[view]) 117 | 118 | # View 119 | fov = 60. 120 | up = 0,0,1 121 | def pullback(fraction): 122 | global origin 123 | origin += fraction*(asarray(origin)-target) 124 | if view()=='front': 125 | origin = -49.412825379531753,208.92850880962186,78.324132111435546+5 126 | target = 1.8198107879612146,-9.5738105894434717,53.519032449873805+5 127 | pullback(.1) 128 | elif view()=='down': 129 | origin = -35.099345315100472,-112.56477679297062,151.73361854587179 130 | target = -0.4743525601077685,-11.413973723185453,0.44285274279076814 131 | pullback(.2) 132 | elif view()=='side': 133 | origin = -39.679084283641487,-239.85430391769961,75.543129644474547 134 | target = 5.2339400073202738,-10.984723621310748,11.128750338448013 135 | shift = projected_orthogonal_to_unit_direction(asarray(target)-origin,(0,0,1)) 136 | shift = 10*normalized(Rotation.from_angle_axis(pi/2,(0,0,1))*shift) 137 | origin += shift 138 | target += shift 139 | pullback(.1) 140 | elif view()=='canopy': 141 | origin = 15.937630749436821,-10.55833672257492,60.718947647935565 142 | target = 18.266964477314549,-10.578622900392148,71.818862427738665 143 | elif view()=='terdragon-side': 144 | origin = -70.331627112440813,164.23717989126743,65.999483565575716 145 | target = 0.57691794482902314,-7.4761573866077597,26.788706998314769 146 | pullback(.2) 147 | elif view()=='terdragon-down': 148 | origin = -49.519740925552036,111.48691471496269,157.32456439198199 149 | target = -6.34304308356056,9.1542087044817428,7.7826856731907048 150 | pullback(.2) 151 | elif view()=='koch-front': 152 | origin = -46.082316229740172,179.20323643974311,73.74081360141966 153 | target = -0.47139844334676173,-7.465299519683386,26.072048830404896 154 | pullback(.15) 155 | elif view()=='koch-side': 156 | origin = 0.22170089213766708,123.93437270853076,180.33272525228978 157 | target = -6.5876712447798162,-17.224156513626859,44.43756566063562 158 | pullback(.2) 159 | origin = target+Rotation.from_angle_axis(-10*pi/180,(0,0,1))*(origin-target) 160 | elif view()=='gosper-front': 161 | origin = -37.548031375625783,174.33524984605464,94.06447355517362 162 | target = 2.9977420980846889,-0.88814677571724876,21.815315250732041 163 | pullback(.2) 164 | elif view()=='gosper-back': 165 | origin = -37.548031375625783,174.33524984605464,94.06447355517362 166 | target = 2.9977420980846889,-0.88814677571724876,21.815315250732041 167 | pullback(.2) 168 | sun_direction.set(normalized(normalized(origin-target)+(0,0,.25))) 169 | elif view()=='gosper-side': 170 | origin = 106.14852758526946,184.76928051872716,86.739813112730673 171 | target = -1.8413109040436795,8.757791108450121,17.952745587243811 172 | pullback(.2) 173 | elif view()=='gosper-other': 174 | origin = -128.8615749233883,-80.825171896664514,131.82615051315565 175 | target = 4.2833936284253209,-10.03950863326339,21.144571304972139 176 | pullback(.3) 177 | elif view()=='sierpinski-thick': 178 | origin = 49.790814279239328,37.745462112076865,110.31450473396978 179 | target = 46.830878243261445,33.319459198501335,103.8336842821352 180 | pullback(1.5) 181 | elif view()=='custom': 182 | origin = origin() 183 | target = target() 184 | else: 185 | raise RuntimeError('unknown view %s'%view()) 186 | origin = asarray(origin) 187 | target = asarray(target) 188 | 189 | # Count representatives 190 | reps = int(open(data()+'/count').read()) 191 | 192 | # Generate colors 193 | if multicolor: 194 | random.seed(color_seed()) 195 | colors = wheel_color(random.uniform(0,1,size=max(1,reps))) 196 | if any(simple_color()): 197 | colors[:] = simple_color() 198 | 199 | # Rotate y to z 200 | ytoz = Rotation.from_rotated_vector((0,1,0),(0,0,1)) 201 | 202 | # Sun details 203 | sun_closer_factor = 1e-4 204 | sun_distance = sun_closer_factor * 1.496e11 205 | sun_radius = sun_closer_factor * 6.955e8 206 | 207 | # Tile details 208 | if tile_id()>=0: 209 | image_shape = array([width(),height()]) 210 | tiles = (image_shape+tile_size()-1)//tile_size() 211 | tile = array([tile_id()//tiles[1],tile_id()%tiles[1]]) 212 | assert all(0<=tile) and all(tile=0 else '') 241 | open(scene_file,'w').write(scene_version('0.4.1', 242 | {'pssmlt': 243 | integrator('pssmlt', 244 | fields(maxDepth=depth(),twoStage=two_stage())), 245 | 'erpt': 246 | integrator('erpt', 247 | fields(maxDepth=depth(),manifoldPerturbation=True,lensPerturbation=False,multiChainPerturbation=False,causticPerturbation=False, 248 | chainLength=200,directSamples=128,numChains=4.,probFactor=120.)), 249 | 'path': 250 | integrator('path', 251 | fields(maxDepth=depth())), 252 | 'direct': 253 | integrator('direct', 254 | fields(maxDepth=100,rrDepth=5,luminaireSamples=1,bsdfSamples=1)) 255 | }[integrator_type()], 256 | 257 | sensor('perspective', 258 | fields(nearClip=0.1, farClip=30000., fov=fov), 259 | transform('toWorld', 260 | lookAt(origin=origin,target=target,up=up)), 261 | film('hdrfilm', 262 | fields(width=width(),height=height(),banner=False), 263 | skipif(tile_id()<0, 264 | fields(cropOffsetX=tile_start[0],cropOffsetY=tile_start[1],cropWidth=tile_size[0],cropHeight=tile_size[1])), 265 | rfilter('gaussian')), 266 | samp), 267 | 268 | skipif(sun_only() and sphere_sun(), 269 | emitter('sky' if sphere_sun() else 'sunsky', 270 | fields(stretch=1.1,sunRadiusScale=sun_scale(), 271 | scale=(.1 if view()=='sierpinski-thick' else 1.)), 272 | vector('sunDirection',sun_direction()), 273 | transform('toWorld',rotate(ytoz)))), 274 | 275 | skipif(not sphere_sun(), 276 | shape('sphere', 277 | fields(radius=sun_scale()*sun_radius), 278 | point('center',sun_distance*normalized(sun_direction())), 279 | emitter('area', 280 | fields(samplingWeight=10.), 281 | rgb('radiance',0.00327988/max(1e-20,sun_scale())*array([6419653., 5444225., 4617994.5]))))), 282 | 283 | skipif(view()!='canopy', 284 | emitter('point', 285 | point('position',origin-.01*(target-origin)), 286 | spectrum('intensity',5.))), 287 | 288 | # Ground 289 | skipif(view()=='canopy', 290 | shape('disk', 291 | transform('toWorld',scale(100000*ones(3))), 292 | twosided('ground', 293 | select(1, 294 | bsdf('ground'+inner_suffix,'plastic', 295 | rgb('diffuseReflectance',.1*ones(3))), 296 | bsdf('ground'+inner_suffix,'conductor', 297 | fields(material='Au')))))), 298 | 299 | # Gold 300 | twosided('mono', 301 | select(1, 302 | bsdf('mono'+inner_suffix,'conductor', 303 | fields(material='Au')), 304 | bsdf('mono'+inner_suffix,'roughconductor', 305 | fields(material='Au',alpha=.01)))), 306 | 307 | select(extra(), 308 | # Extra mesh 309 | shape('obj', 310 | fields(filename='extra.obj',maxSmoothAngle=30.), 311 | one_bsdf(0)), 312 | 313 | select(reps==0, 314 | shape('obj', 315 | fields(filename='single.obj',maxSmoothAngle=30.), 316 | one_bsdf(0)), 317 | concat(( 318 | # Shape groups 319 | concat((shapegroup('group-%d'%i, 320 | shape('obj', 321 | fields(filename='rep-%d.obj'%i,faceNormals=facets()), 322 | one_bsdf(i)) 323 | ) for i in xrange(reps))), 324 | # Instances 325 | concat((include('instances-%d.xml'%i) for i in xrange(reps))))))))) 326 | 327 | # Run mitsuba 328 | if not norender(): 329 | if not output(): 330 | output.set(view()+'.exr') 331 | if gui(): 332 | cmd = ['mtsgui']+options().split()+[scene_file] 333 | else: 334 | cmd = ['mitsuba','-o',output()]+options().split()+[scene_file] 335 | print ' '.join(cmd) 336 | subprocess.call(cmd) 337 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [pytest] 2 | minversion = 2.1 3 | norecursedirs = .* build dist site_scons output* 4 | addopts = -v --tb=short --doctest-glob='' 5 | -------------------------------------------------------------------------------- /sierpinski: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import division,print_function,unicode_literals 4 | from geode.value import parser 5 | from fractal_helper import dihedral_angle_range 6 | from geode import * 7 | import subprocess 8 | import sys 9 | import re 10 | 11 | # Properties 12 | props = PropManager() 13 | resolution = props.add('resolution',6) 14 | border = props.add('border',10) 15 | levels = props.add('levels',10) 16 | parser.parse(props,'Sierpinski carpet gif generator',positional=[]) 17 | 18 | border = border() 19 | resolution = resolution() 20 | 21 | def carpet(n): 22 | '''Make a Sierpinski carpet with n levels''' 23 | r = 3**resolution 24 | if n == 0: 25 | return ones((r,r)) 26 | sub = carpet(n-1) 27 | s = r//3 28 | sub = sub.reshape(s,3,s,3).mean(1).mean(-1) 29 | lo = concatenate([sub,sub,sub]) 30 | mid = concatenate([sub,zeros((s,s)),sub]) 31 | C = concatenate([lo,mid,lo],axis=-1) 32 | assert C.shape==(r,r) 33 | return C 34 | 35 | r = 3**resolution 36 | m = 2*border+r 37 | frames = [] 38 | for n in xrange(levels()): 39 | C = carpet(n) 40 | I = ones((m,m,3)) 41 | I[border:border+r,border:border+r] = 1+C[...,None]*(-1,-1,0) 42 | name = 'sierpinski-%d.png'%n 43 | Image.write(name,I) 44 | frames.append(name) 45 | subprocess.check_call('convert -delay 20 -loop 0'.split()+frames+['sierpinski.gif']) 46 | -------------------------------------------------------------------------------- /union: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import division,print_function,unicode_literals 4 | from geode import * 5 | from geode.value import parser 6 | from tim.otherfab import * 7 | 8 | props = PropManager() 9 | meshes = props.add('meshes',None).set([]) 10 | output = props.add('output','').set_abbrev('o').set_required(1) 11 | parser.parse(props,'Union a bunch of meshes',positional=[meshes]) 12 | 13 | # Read meshes 14 | tms = [] 15 | for m in meshes(): 16 | tm = TriMesh() 17 | tm.read(m) 18 | tms.append(tm) 19 | 20 | # Union 21 | tm = csg_union(tms) 22 | 23 | # Write 24 | tm.write(output()) 25 | --------------------------------------------------------------------------------