File indexing completed on 2026-04-09 07:49:00
0001
0002 """
0003 phicut.py
0004 ============
0005
0006
0007 ~/o/CSG/phicut.sh
0008
0009
0010 """
0011 import numpy as np
0012 import logging
0013 from opticks.CSG.scan import Scan
0014
0015 log = logging.getLogger(__name__)
0016
0017 try:
0018 import pyvista as pv
0019 except ImportError:
0020 pv = None
0021 pass
0022
0023
0024 class HalfPlane(object):
0025
0026 @classmethod
0027 def phi_quadrant(cls, phi):
0028 """
0029 This way of defining the quadrant avoids precision of pi issues.
0030 Yes, but its better to use the same definition that can be applied to the
0031 intersect positions without having to calculate phi from x,y.
0032 Problems likely at boundaries between quadrants
0033
0034 0.5
0035 0.75 | 0.25
0036 |
0037 |
0038 1.0 ------+------- 0.0 , 2.0
0039 |
0040 |
0041 1.25 | 1.75
0042 1.5
0043
0044
0045 cosPhi < 0 | cosPhi > 0
0046 |
0047 -+ | ++
0048 01 | 11
0049 | sinPhi > 0
0050 - - - - +----------------------------
0051 | sinPhi < 0
0052 -- | +-
0053 00 | 10
0054
0055 """
0056 if phi >= 0 and phi < 0.5:
0057 quadrant = 3
0058 elif phi >= 0.5 and phi < 1.0:
0059 quadrant = 1
0060 elif phi >= 1.0 and phi < 1.5:
0061 quadrant = 0
0062 elif phi >= 1.5 and phi <= 2.0:
0063 quadrant = 2
0064 else:
0065 quadrant = -1
0066 pass
0067 return quadrant
0068
0069 @classmethod
0070 def xy_quadrant(cls, x, y ):
0071 xpos = int(x >= 0.)
0072 ypos = int(y >= 0.)
0073 return 2*xpos + ypos
0074
0075 @classmethod
0076 def test_quadrant(cls, n=17):
0077 """
0078 In [3]: np.linspace(0,2,17)
0079 Out[3]: array([0. , 0.125, 0.25 , 0.375, 0.5 , 0.625, 0.75 , 0.875, 1. , 1.125, 1.25 , 1.375, 1.5 , 1.625, 1.75 , 1.875, 2. ])
0080 """
0081 phi = np.linspace(0, 2, n)
0082 for p in phi:
0083 x = np.cos(p*np.pi)
0084 y = np.sin(p*np.pi)
0085 _phi_quadrant = cls.phi_quadrant(p)
0086 _xy_quadrant = cls.xy_quadrant(x,y)
0087 print( " p %10.4f phi_quadrant %d cosPhi %10.4f sinPhi %10.4f xy_quadrant %d " % (p, _phi_quadrant, x, y, _xy_quadrant ))
0088 pass
0089
0090 def __init__(self, phi, debug=False ):
0091 cosPhi, sinPhi = np.cos(phi*np.pi), np.sin(phi*np.pi)
0092
0093
0094 n_quadrant = self.xy_quadrant(cosPhi, sinPhi)
0095 n = np.array( [ sinPhi, -cosPhi, 0. ] )
0096
0097 if debug:
0098 log.info( " phi %10.3f cosPhi %10.3f sinPhi %10.3f n_quadrant %d " % (phi, cosPhi, sinPhi, n_quadrant ))
0099 pass
0100
0101 self.n_quadrant = n_quadrant
0102 self.n = n
0103 self.sinPhi = sinPhi
0104 self.cosPhi = cosPhi
0105 self.debug = debug
0106
0107 def intersect(self, ray_origin, ray_direction, t_min ):
0108 """
0109
0110
0111 phi=1 -(*)- - - - - O-----*--+-----*------ X phi = 0.
0112 / / | \
0113 / / | \
0114 / / n \
0115 / \
0116 /
0117
0118 How to generalize disqualification ? Possible techniques
0119
0120 1. quadrant match
0121
0122 * check the signs of (x,y) at the intersect
0123 corresponds to signs of (cosPhi, sinPhi) of the plane
0124
0125 * issues with this at phi 1.5, 2.0 : presumably from depending on the sign of small values
0126 * using "side > 0" avoids the problems and is simpler
0127
0128 2. rotate intersect (x,y) by -phi, so sign of xrot can be required +ve
0129
0130 * xrot = x*cosPhi + y*sinPhi
0131 * yrot = -x*sinPhi + y*cosPhi
0132
0133 3. intersect must fulfil p.n = 0 yes but disqualified does too.. so no use
0134
0135 4. dot product between the vector from origin to intersect and the vector
0136 (cosPhi, sinPhi, 0 ) that is within the plane and identifies the phi
0137 should be +ve
0138
0139 * side = x*cosPhi + y*sinPhi
0140 * actually the maths of this is the same as backwards rotation
0141 its just thinking of the same thing in two different frames
0142
0143
0144 """
0145 n = self.n
0146 sinPhi = self.sinPhi
0147 cosPhi = self.cosPhi
0148 debug = self.debug
0149
0150 dn = np.dot(ray_direction, n )
0151 on = np.dot(ray_origin, n )
0152 t_cand = t_min if dn == 0. else -on/dn
0153
0154 xyz = ray_origin + t_cand*ray_direction
0155
0156 x = ray_origin[0] + t_cand*ray_direction[0]
0157 y = ray_origin[1] + t_cand*ray_direction[1]
0158 side0 = x*cosPhi + y*sinPhi
0159
0160 side = ray_origin[0]*cosPhi + ray_origin[1]*sinPhi + ( ray_direction[0]*cosPhi + ray_direction[1]*sinPhi )*t_cand
0161
0162 sidecheck = True
0163 if sidecheck:
0164 valid_intersect = t_cand > t_min and side > 0.
0165 else:
0166 valid_intersect = t_cand > t_min
0167 pass
0168
0169 if debug:
0170 afmt_ = lambda a:"%7.3f %7.3f %7.3f" % (a[0], a[1], a[2])
0171 print(" ray_origin %s ray_direction %s xyz %s t_cand %7.3f side0 %10.4f side %10.4f valid_intersect %d" %
0172 (afmt_(ray_origin), afmt_(ray_direction), afmt_(xyz), t_cand, side0, side, valid_intersect ))
0173 pass
0174 if valid_intersect:
0175 isect = np.zeros(4)
0176 isect[:3] = n
0177 isect[3] = t_cand
0178 else:
0179 isect = None
0180 pass
0181 return isect
0182
0183
0184 class PhiCut(object):
0185 @classmethod
0186 def plane(cls, phi):
0187 return np.array( [np.sin(np.pi*phi), -np.cos(np.pi*phi), 0, 0 ], dtype=np.float32 )
0188
0189 def __init__(self, phiStart, phiDelta ):
0190 phi0 = phiStart
0191 phi1 = phiStart + phiDelta
0192
0193 cosPhi0, sinPhi0 = np.cos(phi0*np.pi), np.sin(phi0*np.pi)
0194 cosPhi1, sinPhi1 = np.cos(phi1*np.pi), np.sin(phi1*np.pi)
0195 n0 = np.array( [ sinPhi0, -cosPhi0, 0. ] )
0196 n1 = np.array( [ -sinPhi1, cosPhi1, 0. ] )
0197 self.n0 = n0
0198 self.n1 = n1
0199 self.cosPhi0 = cosPhi0
0200 self.sinPhi0 = sinPhi0
0201 self.cosPhi1 = cosPhi1
0202 self.sinPhi1 = sinPhi1
0203
0204
0205 def intersect(self, ray_origin, ray_direction, t_min ):
0206 """
0207 TODO: add handling of pure-z rays and then mixed 3D rays, add unbounded_exit signally
0208
0209
0210 These are currently full planes need to disqualify half of them.
0211 What exactly is the convention for the allowed intersects ?
0212
0213 The solid lines below denote the valid half planes.
0214 For the infinite plane intersection points [0] [1] (0) (1)
0215
0216
0217 p1 [cosPhi1, sinPhi1]
0218 /
0219 /
0220 (1)
0221 /:
0222 / :
0223 / :
0224 . . .{0}. . . +--(0)--------- p0 [ cosPhi0, sinPhi0 ]
0225 : . :
0226 : . :
0227 : . :
0228 : . :
0229 : . :
0230 : . :
0231 : . :
0232 :. :
0233 {1} :
0234 .: :
0235 . : :
0236 . : :
0237 o o
0238
0239
0240 """
0241 cosPhi0 = self.cosPhi0
0242 sinPhi0 = self.sinPhi0
0243 cosPhi1 = self.cosPhi1
0244 sinPhi1 = self.sinPhi1
0245
0246
0247
0248
0249
0250 d_n0 = ray_direction[0]*sinPhi0 + ray_direction[1]*(-cosPhi0)
0251 d_n1 = ray_direction[0]*(-sinPhi1) + ray_direction[1]*(cosPhi1)
0252 o_n0 = ray_origin[0]*sinPhi0 + ray_origin[1]*(-cosPhi0)
0253 o_n1 = ray_origin[0]*(-sinPhi1) + ray_origin[1]*(cosPhi1)
0254 t0 = t_min if d_n0 == 0. else -o_n0/d_n0
0255 t1 = t_min if d_n1 == 0. else -o_n1/d_n1
0256
0257
0258
0259
0260
0261
0262
0263
0264
0265 side0 = ray_origin[0]*cosPhi0 + ray_origin[1]*sinPhi0 + ( ray_direction[0]*cosPhi0 + ray_direction[1]*sinPhi0 )*t0
0266 side1 = ray_origin[0]*cosPhi1 + ray_origin[1]*sinPhi1 + ( ray_direction[0]*cosPhi1 + ray_direction[1]*sinPhi1 )*t1
0267 if side0 < 0.: t0 = t_min
0268 if side1 < 0.: t1 = t_min
0269
0270 t_near = min(t0,t1)
0271 t_far = max(t0,t1)
0272
0273
0274 t_cand = t_min
0275 if t_near > t_min:
0276 t_cand = t_near
0277 else:
0278 if t_far > t_min:
0279 t_cand = t_far
0280 else:
0281 t_cand = t_min
0282 pass
0283 pass
0284
0285 valid_intersect = t_cand > t_min
0286
0287 if valid_intersect:
0288 isect = np.zeros(4)
0289 isect[0]= -sinPhi1 if t_cand == t1 else sinPhi0
0290 isect[1]= cosPhi1 if t_cand == t1 else -cosPhi0
0291 isect[2]= 0.
0292 isect[3] = t_cand
0293 else:
0294 isect = None
0295 pass
0296 return isect
0297
0298
0299 def check_normals():
0300
0301 phi = np.linspace(0,2,10)
0302
0303 planes = np.zeros( (len(phi), 4), dtype=np.float32 )
0304 pos = np.zeros( (len(phi), 3), dtype=np.float32 )
0305
0306 for i in range(len(phi)):
0307 planes[i] = PhiCut.plane(phi[i])
0308 pos[i] = (10*np.cos(phi[i]*np.pi), 10*np.sin(phi[i]*np.pi), 0 )
0309 pass
0310
0311 size = np.array([1280, 720])
0312 pl = pv.Plotter(window_size=size*2 )
0313
0314 pl.view_xy()
0315 pl.add_arrows( pos, planes[:,:3], color="red" )
0316 pl.add_arrows( pos, -planes[:,:3], color="blue" )
0317
0318 look = (0,0,0)
0319 up = (0,1,0)
0320 eye = (0,0, 10)
0321
0322 pl.set_focus( look )
0323 pl.set_viewup( up )
0324 pl.set_position( eye, reset=True )
0325 pl.camera.Zoom(1)
0326 pl.show_grid()
0327 cp = pl.show()
0328
0329
0330
0331 if __name__ == '__main__':
0332 logging.basicConfig(level=logging.INFO)
0333
0334
0335
0336
0337
0338
0339
0340
0341
0342
0343
0344
0345
0346
0347 geom = PhiCut( 0.0, 0.5 )
0348
0349
0350
0351 modes = [0,1,2,3]
0352
0353
0354 scan = Scan.XY(geom, 100, modes=modes)
0355 Scan.Plot(scan)
0356
0357