I'm trying to make a bunch of geometric objects which have their intrinsic geometric properties (center point, radius, lengths, etc.), as well as properties to help plot them (like x, y, z coordinates for a triangular mesh, arc resolution, etc.).
Since calculating the x, y, z coordinates is an expensive task for some of the shapes (like a triangular prism with edge rounding), I don't want to do it every time a property is changed, but only when the coordinates are requested. Even then though, it shouldn't be necessary to recalculate them if the shape's definition hasn't changed.
So my solution has been to create a "hash" which is is simply a tuple of all parameters which define the shape's "state." If the hash is unchanged, then the previously calculated coordinates can be re-used, otherwise, the coordinates must be recalculated. So I'm using the hash as a way to store the signature or fingerprint of the shape's definition.
I think what I have works, but I wonder if there are more robust ways to handle this that take advantage of __hash__ or id's or something. That feels like overkill to me, but I'm open to suggestions.
Here's my implementation for a sphere. I'm using Mayavi for plotting at the end, which you can skip/ignore if you don't have Mayavi.
#StdLib Imports
import os
#Numpy Imports
import numpy as np
from numpy import sin, cos, pi
class Sphere(object):
"""
Class for a sphere
"""
def __init__(self, c=None, r=None, n=None):
super(Sphere, self).__init__()
#Initial defaults
self._coordinates = None
self._c = np.array([0.0, 0.0, 0.0])
self._r = 1.0
self._n = 20
self._hash = []
#Assign Inputs
if c is not None:
self._c = c
if r is not None:
self._r = r
if n is not None:
self._n = n
#property
def c(self):
return self._c
#c.setter
def c(self, val):
self._c = val
#property
def r(self):
return self._r
#r.setter
def r(self, val):
self._r = val
#property
def n(self):
return self._n
#n.setter
def n(self, val):
self._n = val
#property
def coordinates(self):
self._lazy_update()
return self._coordinates
def _lazy_update(self):
new_hash = self._get_hash()
old_hash = self._hash
if new_hash != old_hash:
self._update_coordinates()
def _get_hash(self):
return tuple(map(tuple, [self._c, [self._r, self._n]]))
def _update_coordinates(self):
c, r, n = self._c, self._r, self._n
dphi, dtheta = pi / n, pi / n
[phi, theta] = np.mgrid[0:pi + dphi*1.0:dphi,
0:2*pi + dtheta*1.0:dtheta]
x = c[0] + r * cos(phi) * sin(theta)
y = c[1] + r * sin(phi) * sin(theta)
z = c[2] + r * cos(theta)
self._coordinates = x, y, z
self._hash = self._get_hash()
if __name__ == '__main__':
from mayavi import mlab
ns = [4, 6, 8, 10, 20, 50]
sphere = Sphere()
for i, n in enumerate(ns):
sphere.c = [i*2.2, 0.0, 0.0]
sphere.n = n
mlab.mesh(*sphere.coordinates, representation='wireframe')
mlab.show()
As suggested, here's a version that uses a dictionary to store the hash as a key:
#StdLib Imports
import os
#Numpy Imports
import numpy as np
from numpy import sin, cos, pi
class Sphere(object):
"""
Class for a sphere
"""
def __init__(self, c=None, r=None, n=None):
super(Sphere, self).__init__()
#Initial defaults
self._coordinates = {}
self._c = np.array([0.0, 0.0, 0.0])
self._r = 1.0
self._n = 20
#Assign Inputs
if c is not None:
self._c = c
if r is not None:
self._r = r
if n is not None:
self._n = n
#property
def c(self):
return self._c
#c.setter
def c(self, val):
self._c = val
#property
def r(self):
return self._r
#r.setter
def r(self, val):
self._r = val
#property
def n(self):
return self._n
#n.setter
def n(self, val):
self._n = val
#property
def _hash(self):
return tuple(map(tuple, [self._c, [self._r, self._n]]))
#property
def coordinates(self):
if self._hash not in self._coordinates:
self._update_coordinates()
return self._coordinates[self._hash]
def _update_coordinates(self):
c, r, n = self._c, self._r, self._n
dphi, dtheta = pi / n, pi / n
[phi, theta] = np.mgrid[0:pi + dphi*1.0:dphi,
0:2 * pi + dtheta*1.0:dtheta]
x = c[0] + r * cos(phi) * sin(theta)
y = c[1] + r * sin(phi) * sin(theta)
z = c[2] + r * cos(theta)
self._coordinates[self._hash] = x, y, z
if __name__ == '__main__':
from mayavi import mlab
ns = [4, 6, 8, 10, 20, 50]
sphere = Sphere()
for i, n in enumerate(ns):
sphere.c = [i*2.2, 0.0, 0.0]
sphere.n = n
mlab.mesh(*sphere.coordinates, representation='wireframe')
mlab.show()
Why not simply keep a flag:
class Sphere(object):
def __init__(self, ...):
...
self._update_coordinates()
...
#c.setter
def c(self, val):
self._changed = True
self._c = val
...
def _lazy_update(self):
if self._changed:
self._update_coordinates()
def _update_coordinates(self):
...
self._changed = False
Related
Now, in addition from my two previous posts ODE implements I try to refactro my code and fix some problems. And I decided, that logically create such classes: Solver,Problem.
So code for ODE_Solver and FE classes finally code and working.
# ODS.py
import numpy as np
class ODE_Solver(object):
def __init__(self, f):
if not callable(f):
raise TypeError('f is not %s, function' % type(f))
self.f = lambda u, x: np.asarray(f(u, x), float)
self.err_sch = None
def solver_st(self):
raise NotImplementedError
def err_st(self):
raise NotImplementedError
def set_initial_condition(self, u0):
if isinstance(u0, (float, int)):
self.neq = 1
u0 = float(u0)
else:
u0 = np.asarray(u0)
self.neq = u0.size
self.u0 = u0
try:
f0 = self.f(self.u0, 0)
except IndexError:
raise IndexError(
'index out of bounds f(u,x). correct index %s' % (str(range(self.neq))))
if f0.size != self.neq:
raise ValueError('f(u,x) returend %d elems, vector u has %d elems' % (f0.size, self.neq))
def solve(self, coord_points, terminate=None):
if terminate is None:
terminate = lambda u, x, step_no: False
if isinstance(coord_points, (float, int)):
raise TypeError('solve: x points not numpy array or numbers.')
self.x = np.asarray(coord_points)
if self.x.size <= 1:
raise ValueError('ODESolver.solve points of coords less than two')
n = self.x.size
if self.neq == 1: # ОДУ
self.u = np.zeros(n)
self.err_sch = np.zeros(n)
else:
self.u = np.zeros((n, self.neq))
self.err_sch = np.zeros((n, self.neq))
self.u[0] = self.u0
self.err_sch[0] = 0
for k in range(n - 1):
self.k = k
self.u[k + 1] = self.solver_st()
self.err_sch[k + 1] = self.err_st()
if terminate(self.u, self.x, self.k + 1):
break
return self.u[:k + 2], self.x[:k + 2]
# ES.py
from ODS import ODE_Solver
import numpy as np
class FE(ODE_Solver):
def solver_st(self):
u, f, k, x = self.u, self.f, self.k, self.x
dx = x[k + 1] - x[k]
u_new = u[k] + dx * f(u[k], x[k])
return u_new
def err_st(self):
u, f, k, x, err_sch = self.u, self.f, self.k, self.x, self.err_sch
dx = x[k + 1] - x[k]
err_sch = np.max(dx)**2
return err_sch
I try to implement class Problem (return ODE and get initial conditions)
import numpy as np
class Problem(object):
def __init__(self, u0, End):
self.u0 = np.asarray(u0)
self.End = End # end point of coords
def __call__(self, u, x):
return (u[1], u[2], u[3], u[4],
- 15 * u[4] - 90 * u[3] - 270 * u[2] - 405 * u[1] - 243 * u[0])
And code class Solver for call numerical scheme, plotting the final result, plot and evaluate error:
import numpy as np
import matplotlib as plt
import ES
import ODS
from ADS import ABM4
from ES import FE
from MLNS import MLN
from RKS import RK4
class Solver(object):
def __init__(self, problem, dx,
method=ES.FE): # choose FE scheme for tetsting
"""
"""
self.problem, self.dx = problem, dx
self.solver = method
#staticmethod
def choose_sch(type):
if type == 1:
method = FE
return method
elif type == 2:
method = RK4
return method
elif type == 3:
method = ABM4
return method
elif type == 4:
method = MLN
return method
else:
raise ValueError('not choose numerical scheme!')
def dsolve(self):
solver = self.method(self.problem)
solver.set_initial_condition(self.problem.u0)
n = int(round(self.problem.End / self.dx))
x_points = np.linspace(0, self.problem.End, n + 1)
self.u, self.x = solver.solve(x_points)
if solver.k + 1 == n:
self.plot()
raise ValueError('not converge this scheme,' % self.problem.End)
def plot(self):
plt.plot(self.x, self.u)
plt.show()
Now, when I call this Solver and Problem
import numpy as np
from ODE_Problem import Problem
from SLV_Prob import Solver
def test():
problem = Problem(u0=[0, 3, -9, -8, 0], End=5)
solver = Solver(problem, dx=0.1)
solver.dsolve()
solver.plot()
if __name__ == '__main__':
test()
I get the error:
Traceback (most recent call last):
File "C:\Fin_Proj_ODE\test2.py", line 14, in <module>
test()
File "C:\Fin_Proj_ODE\test2.py", line 9, in test
solver.dsolve()
File "C:\Fin_Proj_ODE\SLV_Prob.py", line 37, in dsolve
solver = self.method(self.problem)
AttributeError: 'Solver' object has no attribute 'method'
And I dont' understand and suppose what reason of this bug...
So, I have 2 questions for implement this Solver:
How to fix this bug?
How to correct rewrite def choose_sch(type):, that I could to call solver and send args type ( and depending on it, a specific numerical scheme will already be started)?
Question One:
Well, as the error states, your Solver class doesn't have an attribute called "method". Your attribute is actually "solver", so instead of calling
self.method(self.problem)
Try
self.solver(self.problem)
Question Two:
If I'm understanding you correctly, you want to know how you can call the choose_sch method from within the solver constructor and take in a type instead of a method directly. For that, simply do this:
class Solver(object):
def __init__(self, problem, dx, solver_type=1): # choose FE scheme for tetsting
"""
"""
self.problem, self.dx = problem, dx
self.solver = self._choose_sch(solver_type)
#staticmethod
def _choose_sch(solver_type):
methods = {1: FE, 2: RK4, 3: ABM4, 4: MLN}
if solver_type in methods:
return methods[solver_type]
else:
raise ValueError('not choose numerical scheme!')
The dictionary here is much better than the if statement for these kinds of tasks.
You can also alternatively not make _choose_ach a staticmethod if you don't need to call it from a static context and just make it set the solver directly.
I was following this tutorial and after creating the classes I still cannot import the desired module. The code I used:
import numpy as np
import sys
class ForwardEuler:
def __init__(self, f):
# test that f is a function
if not callable(f):
raise TypeError('f is %s, not a function' % type(f))
self.f = f
def set_initial_condition(self, U0):
self.U0 = float(U0)
def solve(self, time_points):
"""Compute u for t values in time_points list."""
self.t = np.asarray(time_points)
self.u = np.zeros(len(time_points))
self.u[0] = self.U0
for k in range(len(self.t)-1):
self.k = k
self.u[k+1] = self.advance()
return self.u, self.t
def advance(self):
"""Advance the solution one time step."""
u, f, k, t = self.u, self.f, self.k, self.t
dt = t[k+1] - t[k]
unew = u[k] + dt*f(u[k], t[k])
return unew
class ODESolver:
def __init__(self, f):
self.f = f
def advance(self):
"""Advance solution one time step."""
raise NotImplementedError # implement in subclass
def set_initial_condition(self, U0):
self.U0 = float(U0)
def solve(self, time_points):
self.t = np.asarray(time_points)
self.u = np.zeros(len(self.t))
# Assume that self.t[0] corresponds to self.U0
self.u[0] = self.U0
# Time loop
for k in range(n-1):
self.k = k
self.u[k+1] = self.advance()
return self.u, self.t
def advance(self):
raise NotImplemtedError # to be impl. in subclasses
class ForwardEuler(ODESolver):
def advance(self):
u, f, k, t = self.u, self.f, self.k, self.t
dt = t[k+1] - t[k]
unew = u[k] + dt*f(u[k], t)
return unew
Now, I want from ODESolver import ForwardEuler, but there is no module named ODESolver. How do I create it? I suppose there must be something with if __name__ == '__main__': and then the classes underneath, but that didn't work either.
the name of the file.py where you are writting this code is the name of module
you must name it ODESolver
then you can do
from ODESolver import ForwardEuler
I create an instance of class Vector2 with line AB = Vector2.from_points(A, B)
But python errors out with TypeError: object() takes no parameters
on line AB = Vector2.from_points(A,B)
and on line return Vector2(cls, P2[0]-P1[0], P2[1]-P1[1])
so I figured maybe the book is wrong (I'm looking at examples in a book). I subtract the Vector2 and cls from the def from_points statement so that...
this is how the new line reads: return (P2[0]-P1[0], P2[1]-P1[1])
When I do this a receive the vector value from def from_points equal too (5, 10)
But then python errors out on:
print AB.get_magnitude()
with AttributeError: 'tuple' object has no attribute 'get_magnitude'
so without the code related to Vector2 and cls the program won't read AB as a class object but it seems that I'm not formatting it right so it won't go through.
I have been stuck on this for days.
#Vector Test
import math
class Vector2(object):
def _init_(self, x=0.0,y=0.0):
self.x = x
self.y = y
def _str_(self):
return"(%s,%s)"%(self.x,self.y)
#classmethod
def from_points(cls, P1, P2):
return Vector2(cls, P2[0]-P1[0],P2[1]-P1[1])
def get_magnitude(self):
return math.sqrt(self.x**2 + self.y**2)
A = (15.0, 20.0)
B = (20.0, 30.0)
AB = Vector2.from_points(A, B)
print AB
print AB.get_magnitude()
CHANGED CODE:
#Vector Test
import math
class Vector2(object):
def _init_(self, x=0.0,y=0.0):
self.x = x
self.y = y
def _str_(self):
return"(%s,%s)"%(self.x,self.y)
#classmethod
def from_points(cls, P1, P2):
return (P2[0]-P1[0],P2[1]-P1[1])
def get_magnitude(self):
return math.sqrt(self.x**2 + self.y**2)
A = (15.0, 20.0)
B = (20.0, 30.0)
AB = Vector2.from_points(A, B)
print AB
print AB.get_magnitude()
that's (probably) what you mean.
#Vector Test
import math
class Vector2(object):
def __init__(self, x=0.0,y=0.0):
self.x = x
self.y = y
def __str__(self):
return"(%s,%s)"%(self.x,self.y)
#classmethod
def from_points(cls, P1, P2):
return Vector2(P2.x-P1.x,P2.y-P1.y)
def get_magnitude(self):
return math.sqrt(self.x**2 + self.y**2)
A = Vector2(15.0, 20.0)
B = Vector2(20.0, 30.0)
AB = Vector2.from_points(A, B)
print( AB )
print( AB.get_magnitude() )
I solve a system of two equations in a class property, and it returns the solution -- values of two variables. I'd like both values be a properties of the class -- how do I achieve this without solving the system twice? Here's an example
#!/usr/bin/python3
class Test(object):
pass
def ab(self):
print("Calc!")
a = 1
b = 2
return [a,b]
#property
def a(self):
return self.ab()[0]
#property
def b(self):
return self.ab()[1]
test = Test()
print(test.a)
print(test.b)
It outputs:
Calc!
1
Calc!
2
so it actually "solved" the system of equations (ab property) twice. If it had solved it once, than the output would be:
Calc!
1
2
How do I achieve this?
Edit
Example with a system:
#!/usr/bin/python3
import scipy
from scipy.optimize import fsolve
class Test(object):
def __init__(self, c, d):
self.c = c
self.d = d
def ab(self):
print("Calc!")
result = fsolve(lambda x: [
x[0] + 2*x[1] + self.c
, 3*x[0] - x[1] + self.d
], [1,1])
return result
#property
def a(self):
return self.ab()[0]
#property
def b(self):
return self.ab()[1]
test = Test(-5,2)
print(test.a)
print(test.b)
Gives:
Calc!
0.142857142857
Calc!
2.42857142857
I want it to solve a system only once:
Calc!
0.142857142857
2.42857142857
Edit 2
Real code:
#!/usr/bin/env python3
import argparse, os, sys
# ==============
## parsing args:
parser = argparse.ArgumentParser()
argsLip = parser.add_argument_group('Properties of lipid:')
argsLip.add_argument('-A', '--area',
help = "incompressible area, Ų (default to %(default)s)",
dest = 'A_n',
action = 'store',
type = float,
default = 20.0,
)
argsLip.add_argument('-L',
help = "basic length in Å (default to %(default)s)",
dest = 'L',
action = 'store',
type = float,
default = 15.0,
)
argsLip.add_argument('-K', '--K-coef',
help = "bending rigidity in kTL (default to %(default)s)",
dest = 'K_f_coef',
action = 'store',
type = float,
default = 0.33,
)
argsMem = parser.add_argument_group('Properties of membrane:')
argsMem.add_argument('-g', '--gamma',
help = "surface tension, erg/cm² (default to %(default)s)",
dest = 'γ',
action = 'store',
type = float,
default = 30.0,
)
argsEnv = parser.add_argument_group('Properties of environment:')
argsEnv.add_argument('-T', '--temperature',
help = "temperature, K (default to %(default)s)",
dest = 'T',
action = 'store',
type = float,
default = 323.0,
)
argsCalc = parser.add_argument_group('Calc options:')
argsCalc.add_argument('-a', '--a-trial',
help = "trial value of a to be used in nsolve (default to %(default)s)",
dest = 'a_trial',
action = 'store',
type = float,
default = 2.0,
)
args = parser.parse_args()
# =========
## imports:
# symbolic:
import sympy
from sympy import symbols, solve, nsolve, pprint, diff, S, Function
from sympy import sqrt as Sqrt
sympy.init_printing(pretty_print=True, use_unicode=True, wrap_line=False, no_global=True)
# numeric:
import scipy
from scipy import sqrt
from scipy.optimize import fsolve
# constants:
from scipy import pi as π
from scipy.constants import k as k_SI
k = k_SI * 10**7 # J/K → erg/K
# =========
## program:
class MonoFlexible_symbolic(object):
"This class initiates common symbolic expressions to be used in all MonoFlexible classes."
def __init__(self):
a, l = symbols("a l", real=True, positive=True)
b = Function('b')(a, l)
ν = Function('ν')(l)
equation = (
3 / (4 * b)
+ 1 / ( 2 * Sqrt(2) * b**(S(3)/4) )
- ν * ( Sqrt(a) - 1 )**2
)
equation_diff_a = equation.diff(a)
equation_diff_a2 = equation_diff_a.diff(a)
equation_diff_l = equation.diff(l) .subs(ν.diff(l) , -3*ν)
equation_diff_l2 = equation_diff_l.diff(l) .subs(ν.diff(l,2), 12*ν)
equation_diff_al = equation_diff_a.diff(l) .subs(ν.diff(l) , -3*ν)
db_da = solve( equation_diff_a , b.diff(a) )[0]
d2b_da2 = solve( equation_diff_a2 , b.diff(a,2) )[0]
db_dl = solve( equation_diff_l , b.diff(l) )[0]
d2b_d2l = solve( equation_diff_l2 , b.diff(l,2) )[0]
d2b_dadl = solve( equation_diff_al , b.diff(a).diff(l) )[0]
# print("db_da =")
# pprint(
# db_da
# )
# print("d2b_da2 =")
# pprint("d2b_da2 =",
# d2b_da2
# )
# print("db_dl =")
# pprint(
# db_dl
# )
# print("d2b_dl2 =")
# pprint(
# d2b_d2l
# )
# print("d2b_dadl =")
# pprint(
# cancel(d2b_dadl[0])
# )
self.db_da_func = lambda aa, bb, νν: db_da.subs({
a: aa
, b: bb
, ν: νν
}).evalf()
self.d2b_da2_func = lambda aa, bb, νν: d2b_da2.subs({
a: aa
, b: bb
, ν: νν
}).evalf()
self.db_dl_func = lambda aa, bb, νν: db_dl.subs({
a: aa
, b: bb
, ν: νν
}).evalf()
self.d2b_d2l_func = lambda aa, bb, νν: d2b_dl2.subs({
a: aa
, b: bb
, ν: νν
}).evalf()
self.d2b_dadl_func = lambda aa, bb, νν: d2b_dadl.subs({
a: aa
, b: bb
, ν: νν
}).evalf()
class MonoFlexible(MonoFlexible_symbolic):
def __init__(self,
γ : "Surface tension of the membrane, erg/cm²",
T : "Temperature, K",
L : "Length of the hydrocarbon chain, Å",
A_n : "Non-compressible area of the lipid, Ų",
a_trial : "Initial value for fsolve, default to 2.0" = None,
K_f_coef : "K_f = k T L * K_f_coef, default to 1/3" = None,
) -> "Calculates thermodynamic properties of flexible string":
super().__init__()
self.__γ = γ
self.__T = T
self.__L = L
self.__A_n = A_n
self.__a_trial = a_trial
self.__K_f_coef = K_f_coef
#property
def A_n_Å2(self):
return self.__A_n
#property
def A_n(self):
return self.__A_n * 10**(-16) # Ų → cm²
#property
def L_Å(self):
return self.__L
#property
def L(self):
return self.__L * 10**(-8) # Å → cm
#property
def γ(self):
return self.__γ
#property
def T(self):
return self.__T
#property
def a_trial(self):
"Initial value for numerical equation solving function to find area per lipid."
a_trial = self.__a_trial or 2.0
return a_trial
#property
def K_f_coef(self):
K_f_coef = self.__K_f_coef or 1/3
return K_f_coef
#property
def K_f(self):
"Rigidity of the string."
return k * self.T * self.L * self.K_f_coef
#property
def ν(self):
return self.K_f * self.A_n / (
π * k * self.T * self.L**3
)
#property
def ab(self):
print("Calc!")
ab = fsolve( lambda x: [
3 / ( 4 * x[1] )
+ 1 / ( 2 * sqrt(2) * x[1]**(3/4) )
- self.ν * (sqrt(x[0]) - 1)**2
,
- k * self.T / self.A_n * self.db_da_func(x[0], x[1], self.ν) * self.ν * (sqrt(x[0]) - 1)**2
- self.γ
]
, [2., 300.] )
return ab
#property
def a(self):
return self.ab[0]
#property
def b(self):
return self.ab[1]
# ======
## calc:
def main():
flexible_kwargs = {
"γ" : args.γ,
"T" : args.T,
"L" : args.L,
"A_n" : args.A_n,
"a_trial" : args.a_trial,
"K_f_coef" : args.K_f_coef,
}
flexible = MonoFlexible(**flexible_kwargs)
print( "ν = {ν:.5f}".format(ν=flexible.ν) )
print( "a = {a:.2f}".format(a=flexible.a) )
print( "b = {b:.2f}".format(b=flexible.b) )
# python code run faster in a function:
if __name__ == "__main__":
main()
Works with default parameters, so in order to test it -- just run it.
Sounds to me like you're just trying to cache the solution. Here's a way to do that that involves creating another property:
class Test(object):
def __init__(self):
pass
#property
def solution(self):
try:
return self._solution
except AttributeError:
self._solution = self.ab()
return self._solution
def ab(self):
print("Calc!")
a = 1
b = 2
return [a,b]
#property
def a(self):
return self.solution[0]
#property
def b(self):
return self.solution[1]
test = Test()
print(test.a)
print(test.b)
Output:
Calc!
1
2
Update!
In Python 3.8 a built-in decorator to do this was added to the standard library named functools.cached_property() that makes implementing this scheme in an easier and more succinct way (plus it's thread-safe):
import functools
class Test(object):
def __init__(self):
pass
#functools.cached_property
def ab(self):
print("Calc!")
a = 1
b = 2
return [a,b]
#property
def a(self):
return self.ab[0]
#property
def b(self):
return self.ab[1]
assigning the properties as the results are created could be a way to go
#!/usr/bin/python3
class Test(object):
def __init__(self):
pass
def ab(self):
print("Calc!")
self._a = a = 1
self._b = b = 2
return [a,b]
#property
def a(self):
return self._a
#a.setter
def a(self, value):
self._a = value
#property
def b(self):
return self._b
#b.setter
def b(self, value):
self._b = value
test = Test()
results = test.ab()
print(test.a)
print(test.b)
Your current class doesn't have any statefulness at all, so it doesn't seem like an example of good class design. Its exact operation is completely hard-coded...it is hard to know what advantage you get from having it.
Perhaps what you want is something more along the lines of
class Test(object):
def __init__(self):
self.a, self.b = fsolve(lambda x: [
x[0] + 2*x[1] - 5
, 3*x[0] - x[1] + 2
], [1,1])
I have spent the last couple of weeks in my off-time looking at openGL. And while I do not have a problem following some of the older NeHe examples, from everything I have read, OpenGL4 is a totally different process. And I have access to the red book and the super bible, but the former is still offering legacy opengl calls where as the latter uses their own library. Neither is especially helpful in understanding how to put together code in a project. For example, my current understanding is that glu and glut are legacy and shouldn't be used for opengl 4.
I can generate vertices very easily for a hypothetical model space. I have an extremely hard time understanding how a model ends up showing up on my screen. About 95% of my attempts end up with a black blank screen.
Thanks in advance.
Here's some code:
# primatives.py
from collections import Iterable
from functools import reduce
import operator
import numpy as np
from exc import UnimplementedMethod
class Primative(object):
SIZE = 1 # number of pixels on a default grid
def __init__(self, point=None, *args, **kwargs):
self.point = point if isinstance(point, Iterable) else [0, 0, 0]
self.point = np.array(self.point, dtype=np.float32)
scaler = [self.SIZE/2]*len(self.point)
self.point = (self.point * scaler).tolist()
#property
def active(self):
attr = "__active__"
if not hasattr(self, attr):
setattr(self, attr, False)
return getattr(self, attr)
#active.setter
def active(self, value):
attr = "__active__"
if value in [True, False]:
setattr(self, attr, value)
return getattr(self, attr)
#property
def vertices(self):
"""Returns a simple list of calculated vertices"""
clsname = self.__class__.__name__
raise UnimplementedMethod(clsname)
#property
def dimension(self):
return len(self.point)
#property
def scaler(self):
attr = "__scaler__"
if not hasattr(self, attr):
size = self.SIZE / 2
setattr(self, attr, [size]*self.dimension)
return getattr(self, attr)
#scaler.setter
def scaler(self, *values):
attr = "__scaler__"
values = values[0] if len(values) == 1 else values
if len(values) == 1 and len(values) != self.point:
if isinstance(values, [int, float]):
setattr(self, attr, [values]*self.dimension)
elif isinstance(values, Iterable):
data = [(v, i)
for v, i in zip(values, xrange(self.dimension))]
value = [v for v, i in data]
if len(value) != self.dimension:
raise ValueError
setattr(self, attr, value)
#property
def translation(self):
attr = "__transalation__"
if not hasattr(self, attr):
size = self.SIZE / 2
setattr(self, attr, [size]*self.dimension)
return getattr(self, attr)
#translation.setter
def transalation(self, *values):
attr = "__transalation__"
values = values[0] if len(values) == 1 else values
if isinstance(values, (int, float)):
setattr(self, attr, [values]*self.dimension)
elif isinstance(values, Iterable):
data = [(v, i)
for v, i in zip(values, xrange(self.dimension))]
value = [v for v, i in data]
if len(value) != self.dimension:
raise ValueError
setattr(self, attr, value)
#property
def rotation(self):
"""
Rotation in radians
"""
attr = "__rotation__"
if not hasattr(self, attr):
setattr(self, attr, [0]*self.dimension)
return getattr(self, attr)
#rotation.setter
def rotation(self, *values):
"""
Rotation in radians
"""
attr = "__rotation__"
values = values[0] if len(values) == 1 else values
if isinstance(values, (int, float)):
setattr(self, attr, [values]*self.dimension)
elif isinstance(values, Iterable):
data = [(v, i)
for v, i in zip(values, xrange(self.dimension))]
value = [v for v, i in data]
if len(value) != self.dimension:
raise ValueError
setattr(self, attr, value)
#property
def volume(self):
clsname = self.__class__.__name__
raise UnimplementedMethod(clsname)
class Cube(Primative):
# G H
# * --------- *
# /| /|
# C / | D / |
# * --------- * |
# | * -------|- *
# | / E | / F
# |/ |/
# * --------- *
# A B
#property
def center_of_mass(self):
"""
Uses density to calculate center of mass
"""
return self.point
#property
def material(self):
clsname = self.__class__.__name__
raise UnimplementedMethod(clsname)
#material.setter
def material(self, value):
clsname = self.__class__.__name__
raise UnimplementedMethod(clsname)
#property
def mass(self):
return self.material.density * self.volume
#property
def volume(self):
func = operator.mul
return reduce(func, self.scaler, 1)
#property
def normals(self):
"""
computes the vertex normals
"""
norm = []
if len(self.point) == 1:
norm = [
# counter clockwise
# x (left hand rule)
(-1), # A
(1) # B
]
elif len(self.point) == 2:
norm = [
# counter clockwise
# x, y (left hand rule)
(-1, -1), # A
(1, -1), # B
(1, 1), # C
(-1, 1) # D
]
elif len(self.point) == 3:
norm = [
# counter clockwise
# x, y, z (left hand rule)
(-1, -1, 1), # A 0
(1, -1, 1), # B 1
(1, 1, 1), # D 2
(-1, 1, 1), # C 3
(-1, -1, -1), # E 4
(1, -1, -1), # F 5
(1, 1, -1), # H 6
(-1, 1, -1), # G 7
]
return norm
#property
def indices(self):
indices = []
if len(self.point) == 2:
indices = [
[[1, 0, 3], [2, 3, 1]], # BAC CDB front
]
elif len(self.point) == 3:
indices = [
[[1, 0, 3], [2, 3, 1]], # BAC CDB front
[[5, 1, 2], [2, 6, 5]], # FBD DHF right
[[4, 5, 6], [6, 7, 4]], # EFH HGE back
[[5, 4, 0], [0, 1, 5]], # FEA ABF bottom
[[0, 4, 7], [7, 3, 0]], # AEG GCA left
[[2, 3, 7], [7, 6, 2]], # DCG GHD top
]
return indices
#property
def nodes(self):
normals = np.array(self.normals, dtype=np.float32)
scaler = np.array(self.scaler, dtype=np.float32)
nodes = normals * scaler
return nodes.tolist()
#property
def vertices(self):
verts = (n for node in self.nodes for n in node)
return verts
And one more:
# Voxel.py
from collections import Iterable
from time import time
import numpy as np
import pyglet
from pyglet.gl import *
from primatives import Cube
import materials
class Voxel(Cube):
"""
Standard Voxel
"""
def __init__(self, point=None, material=None):
super(Voxel, self).__init__(point=point)
if isinstance(material, materials.Material):
self.material = material
else:
self.material = materials.stone
def __str__(self):
point = ", ".join(str(p) for p in self.point)
material = self.material.name
desc = "<Voxel [%s] (%s)>" % (material, point)
return desc
def __repr__(self):
point = ", ".join(str(p) for p in self.point)
material = self.material.name
desc = "<Voxel %s(%s)>" % (material, point)
return desc
#property
def material(self):
attr = "__material__"
if not hasattr(self, attr):
setattr(self, attr, materials.ether)
return getattr(self, attr)
#material.setter
def material(self, value):
attr = "__material__"
if value in materials.valid_materials:
setattr(self, attr, value)
return getattr(self, attr)
class Chunk(Cube):
"""
A Chunk contains a specified number of Voxels. Chunks are an
optimization to manage voxels which do not change often.
"""
NUMBER = 16
NUMBER_OF_VOXELS_X = NUMBER
NUMBER_OF_VOXELS_Y = NUMBER
NUMBER_OF_VOXELS_Z = NUMBER
def __init__(self, point=None):
point = (0, 0, 0) if point is None else point
super(Chunk, self).__init__(point=point)
self.batch = pyglet.graphics.Batch()
points = []
x_scale = self.NUMBER_OF_VOXELS_X / 2
y_scale = self.NUMBER_OF_VOXELS_Y / 2
z_scale = self.NUMBER_OF_VOXELS_Z / 2
self.rebuild_mesh = True
if len(point) == 1:
points = ((x,) for x in xrange(-x_scale, x_scale))
elif len(point) == 2:
points = ((x, y)
for x in xrange(-x_scale, x_scale)
for y in xrange(-y_scale, y_scale))
elif len(point) == 3:
points = ((x, y, z)
for x in xrange(-x_scale, x_scale)
for y in xrange(-y_scale, y_scale)
for z in xrange(-z_scale, z_scale))
t = time()
self.voxels = dict((point, Voxel(point)) for point in points)
self.active_voxels = dict((p, v)
for p, v in self.voxels.iteritems()
if v.active)
self.inactive_voxels = dict((p, v)
for p, v in self.voxels.iteritems()
if not v.active)
print 'Setup Time: %s' % (time() - t)
#property
def material(self):
return ether
#material.setter
def material(self, value):
if value in materials.valid_materials:
for voxel in self.voxels:
if voxel.material != value:
voxel.material = value
self.rebuild_mesh = True
#property
def mesh(self):
"""
Returns the verticies as defined by the Chunk's Voxels
"""
attr = "__mesh__"
if self.rebuild_mesh == True:
self.mesh_vert_count = 0
vertices = []
t = time()
for point, voxel in self.active_voxels.iteritems():
if voxel.active is True:
vertices.extend(voxel.vertices)
num_verts_in_voxel = len(voxel.normals)
self.mesh_vert_count += num_verts_in_voxel
print "Mesh Generation Time: %s" % time() - t
vertices = tuple(vertices)
setattr(self, attr, vertices)
voxel_count = len(self.active_voxels)
voxel_mesh = self.mesh
count = self.mesh_vert_count
group = None
data = ('v3f/static', vertices)
self.batch.add(count, self.mode, group, data)
return getattr(self, attr)
#property
def center_of_mass(self):
"""
Uses density to calculate center of mass. This is probably only
useful if the chunk represents an object.
"""
center = self.point
points = []
for point, voxel in self.active_voxels.iteritems():
mass = voxel.mass
if mass > 0:
point = [p*mass for p in point]
points.append(point)
points = np.array(points)
means = []
if points.any():
for idx, val in enumerate(self.point):
means.append(np.mean(points[:, idx]))
if means:
center = means
return center
def add(self, voxel):
added = False
point = None
if isinstance(voxel, Voxel):
point = voxel.point
elif isinstance(voxel, Iterable):
point = voxel
if point in self.inactive_voxels.iterkeys():
last = self.voxels[point]
self.voxels[point] = voxel if isinstance(voxel, Voxel) else last
self.voxels[point].active = True
self.active_voxels[point] = self.voxels[point]
self.inactive_voxels.pop(point)
added = True
self.rebuild_mesh = True
return added
def remove(self, voxel):
removed = False
point = None
if isinstance(voxel, Voxel):
point = voxel.point
elif isinstance(voxel, Iterable):
point = voxel
if point in self.active_voxels.iterkeys():
last = self.voxels[point]
self.voxels[point] = voxel if isinstance(voxel, Voxel) else last
self.voxels[point].active = False
self.inactive_voxels[point] = self.voxels[point]
self.active_voxels.pop(point)
removed = True
self.rebuild_mesh = True
return removed
def render(self):
voxels = len(self.active_voxels)
self.batch.draw()
return voxels
if __name__ == "__main__":
import pyglet
from pyglet.gl import *
class Window(pyglet.window.Window):
def __init__(self, *args, **kwargs):
super(Window, self).__init__(*args, **kwargs)
vox_cnt = self.setup_scene()
print 'Added: %s voxels' % (vox_cnt)
def run(self):
"""wrapper to start the gui loop"""
pyglet.app.run()
def setup_scene(self):
self.chunk = Chunk()
cnt = 0
t = time()
for x in xrange(self.chunk.NUMBER_OF_VOXELS_X):
for y in xrange(self.chunk.NUMBER_OF_VOXELS_Y):
self.chunk.add((x, y))
cnt += 1
print "Setup Scene Time: %s" % (time() - t)
return cnt
def render_scene(self):
y = h = self.height
x = w = self.width
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
glMatrixMode(GL_PROJECTION)
glLoadIdentity()
# glEnable(GL_DEPTH_TEST)
# glDepthFunc(GL_LESS)
t = time()
voxels_drawn = self.chunk.render()
print 'Render Time: %s' % (time() - t)
print 'Points Rendered %s' % voxels_drawn
# array_len = len(self.vertex_data)
# glDrawArrays(GL_TRIANGLES, 0, array_len)
def on_draw(self, *args, **kwargs):
self.render_scene()
w = Window()
w.run()
There are examples in the source distributions, if you download them (link to page).
The one you want to see is in <top-dir>/examples/opengl.py -- for a torus. If you make the following modifications you will have a cube.
# line 91:
cube.draw() # previously torus.draw()
# line 178: replace the line with the below (GL_TRIANGLES for GL_QUADS)
glDrawElements(GL_QUADS, 24, GL_UNSIGNED_INT, indices)
# line 187:
cube = Cube(0.8) # previously torus = Torus(1, 0.3, 50, 30)
# replace lines 125 through 166 with:
class Cube(object):
Vertices =(0.,0.,0., 1.,0.,0., 0.,0.,1., 1.,0.,1.,
0.,1.,0., 1.,1.,0., 0.,1.,1., 1.,1.,1.)
def __init__(self, scale):
# Create the vertex and normal arrays.
indices = [0,1,3,2, 1,5,7,3, 5,4,6,7,
0,2,6,4, 0,4,5,1, 2,3,7,6]
normals = [ 0.0, -1.0, 0.0,
1.0, 0.0, 0.0,
0.0, 1.0, 0.0,
-1.0, 0.0, 0.0,
0.0, 0.0, -1.0,
0.0, 0.0, 1.0]
vertices = [scale * v for v in Cube.Vertices]
vertices = (GLfloat * len(vertices))(*vertices)
normals = (GLfloat * len(normals))(*normals)
I don't know about python's class wrappers and I did not do any graphics programming for quite some time. But I know that you should search for qualified answers about real workings and internals in the community of hardware guys who either create the VHDL GPU code or write low level drivers or so. They KNOW for sure how it works and some FAQ explanation should be available already in their community.
Based on that assumption this is what some Googling gave me to start with:
OpenGL 4.4 API Reference Card - page 7 - (available among top level resources on http://www.opengl.org) shows some simple picture (for 5 years old?) with the rendering pipeline split into
Blue blocks indicate various buffers that feed or get fed by the OpenGL pipeline
Green blocks indicate fixed function stages
Yellow blocks indicate programmable stages
Jarrred Walton's - Return of the DirectX vs. OpenGL Debates points to a 130-page slideshow How OpenGL Can Unlock 15x Performance Gains | NVIDIA Blog. Both articles fall into categories AMD,Intel,NVIDIA,Game Developer Converence
I have dome some simple OpenGL using C, C++, Delphi long ago and my recommendation is to get rid of the python mapping at first altogether. Look for suitable class library with good community with some good support only afterwards you know what you are looking for.
The above are IMHO the waters to start fishing in