def _generate_rays(self, template, ray_count): origin_vectors = self._sphere_sampler(ray_count) directions = self._vector_sampler(ray_count) rays = [] for n in range(ray_count): # calculate surface point origin = origin_vectors[n].copy() origin.length = self._radius origin = Point3D(*origin) # calculate surface normal normal = -origin_vectors[n] # transform sampling direction from surface space direction = directions[n].transform( rotate_basis(normal, normal.orthogonal())) # USE WITH HEMISPHERECOSINESAMPLER # cosine weighted distribution, projected area weight is # implicit in distribution, so set weight appropriately rays.append((template.copy(origin, direction), 0.5)) return rays
def _generate_rays(self, template, ray_count): origin_vectors = self._sphere_sampler(ray_count) directions = self._vector_sampler(ray_count) rays = [] for n in range(ray_count): # calculate surface point origin = origin_vectors[n].copy() origin.length = self._radius origin = Point3D(*origin) # calculate surface normal normal = -origin_vectors[n] # transform sampling direction from surface space direction = directions[n].transform(rotate_basis(normal, normal.orthogonal())) # USE WITH HEMISPHERECOSINESAMPLER # cosine weighted distribution, projected area weight is # implicit in distribution, so set weight appropriately rays.append((template.copy(origin , direction), 0.5)) return rays
def point(self, value): if not (self._direction.x == 0 and self._direction.y == 0 and self._direction.z == 1): up = Vector3D(0, 0, 1) else: up = Vector3D(1, 0, 0) self._point = value self._observer.transform = translate( value.x, value.y, value.z) * rotate_basis(self._direction, up)
def direction(self, value): if value.x != 0 and value.y != 0 and value.z != 1: up = Vector3D(0, 0, 1) else: up = Vector3D(1, 0, 0) self._direction = value self._observer.transform = translate(self._point.x, self._point.y, self._point.z) * rotate_basis( value, up)
def __init__(self, slit_id, centre_point, basis_x, dx, basis_y, dy, dz=0.001, parent=None, csg_aperture=False, curvature_radius=0): # perform validation of input parameters if not isinstance(dx, (float, int)): raise TypeError("dx argument for BolometerSlit must be of type float/int.") if not dx > 0: raise ValueError("dx argument for BolometerSlit must be greater than zero.") if not isinstance(dy, (float, int)): raise TypeError("dy argument for BolometerSlit must be of type float/int.") if not dy > 0: raise ValueError("dy argument for BolometerSlit must be greater than zero.") if not isinstance(centre_point, Point3D): raise TypeError("centre_point argument for BolometerSlit must be of type Point3D.") if not isinstance(curvature_radius, (float, int)): raise TypeError("curvature_radius argument for BolometerSlit " "must be of type float/int.") if curvature_radius < 0: raise ValueError("curvature_radius argument for BolometerSlit " "must not be negative.") if not isinstance(basis_x, Vector3D): raise TypeError("The basis vectors of BolometerSlit must be of type Vector3D.") if not isinstance(basis_y, Vector3D): raise TypeError("The basis vectors of BolometerSlit must be of type Vector3D.") self._centre_point = centre_point self._basis_x = basis_x.normalise() self.dx = dx self._basis_y = basis_y.normalise() self.dy = dy self.dz = dz self._curvature_radius = curvature_radius # NOTE - target primitive and aperture surface cannot be co-incident otherwise numerics will cause Raysect # to be blind to one of the two surfaces. slit_normal = basis_x.cross(basis_y) transform = translate(centre_point.x, centre_point.y, centre_point.z) * rotate_basis(slit_normal, basis_y) super().__init__(parent=parent, transform=transform, name=slit_id) self.target = Box(lower=Point3D(-dx/2*1.01, -dy/2*1.01, -dz/2), upper=Point3D(dx/2*1.01, dy/2*1.01, dz/2), transform=None, material=NullMaterial(), parent=self, name=slit_id+' - target') self._csg_aperture = None self.csg_aperture = csg_aperture # round off the detector corners, if applicable if self._curvature_radius > 0: mask_corners(self)
def __init__(self, slit_id, centre_point, basis_x, dx, basis_y, dy, dz=0.001, parent=None, csg_aperture=False): self._centre_point = centre_point self._basis_x = basis_x.normalise() self.dx = dx self._basis_y = basis_y.normalise() self.dy = dy self.dz = dz # NOTE - target primitive and aperture surface cannot be co-incident otherwise numerics will cause Raysect # to be blind to one of the two surfaces. slit_normal = basis_x.cross(basis_y) transform = translate(centre_point.x, centre_point.y, centre_point.z) * rotate_basis( slit_normal, basis_y) super().__init__(parent=parent, transform=transform, name=slit_id) self.target = Box(lower=Point3D(-dx / 2 * 1.01, -dy / 2 * 1.01, -dz / 2), upper=Point3D(dx / 2 * 1.01, dy / 2 * 1.01, dz / 2), transform=None, material=NullMaterial(), parent=self, name=slit_id + ' - target') self._csg_aperture = None self.csg_aperture = csg_aperture
from cherab.tools.plasmas.slab import build_slab_plasma from renate.cherab_models import RenateBeamEmissionLine, RenateBeam world = World() # PLASMA ---------------------------------------------------------------------- plasma = build_slab_plasma(peak_density=5e19, world=world) plasma.b_field = ConstantVector3D(Vector3D(0, 0.6, 0)) # BEAM SETUP ------------------------------------------------------------------ integration_step = 0.0025 beam_transform = translate(-0.5, 0.0, 0) * rotate_basis(Vector3D(1, 0, 0), Vector3D(0, 0, 1)) line = Line(hydrogen, 0, (3, 2)) beam = RenateBeam(parent=world, transform=beam_transform) beam.plasma = plasma beam.energy = 100000 beam.power = 3e6 beam.element = hydrogen beam.temperature = 30 beam.sigma = 0.05 beam.divergence_x = 0. beam.divergence_y = 0. beam.length = 3.0 beam.models = [RenateBeamEmissionLine(line)] beam.integrator.step = integration_step beam.integrator.min_samples = 10
u1 = Point3D(u1x, u1y, u1z) u2x, u2y, u2z = s1_vertices[v2] u2 = Point3D(u2x, u2y, u2z) u3x, u3y, u3z = s1_vertices[v3] u3 = Point3D(u3x, u3y, u3z) uc = Point3D((u1x + u2x + u3x) / 3, (u1y + u2y + u3y) / 3, (u1z + u2z + u3z) / 3) u1u2 = u1.vector_to(u2).normalise() u1u3 = u1.vector_to(u3).normalise() n_pi1 = u1u2.cross(u1u3).normalise() # normal vector of plane 1 debug_vertices = [[u1x, u1y, u1z], [u2x, u2y, u2z], [u3x, u3y, u3z]] debug_triangles = [[0, 1, 2]] # transform to x-y plane s1_transform = translate(u1.x, u1.y, u1.z) * rotate_basis(u1u2, n_pi1) s1_inv_transform = s1_transform.inverse() points = set() for vertex in (u1, u2, u3): tv = vertex.transform(s1_inv_transform) points.add((tv.x, tv.z)) debug_intersection_points = [] for intersection in s1_intersections[s1_tri_id]: s2_tri_id, ut1, ut2 = intersection print('-->') print('s2_tri_id', s2_tri_id) print(ut1.distance_to(ut2))
r, _, z, t_samples = sample3d(h0.distribution.density, (-1, 2, 200), (0, 0, 1), (-1, 1, 200)) plt.imshow(np.transpose(np.squeeze(t_samples)), extent=[-1, 2, -1, 1]) plt.colorbar() plt.axis('equal') plt.xlabel('x axis') plt.ylabel('z axis') plt.title("Neutral Density profile in x-z plane") ########################### # Inject beam into plasma # adas = OpenADAS(permit_extrapolation=True, missing_rates_return_null=True) integration_step = 0.0025 beam_transform = translate(-0.5, 0.0, 0) * rotate_basis( Vector3D(1, 0, 0), Vector3D(0, 0, 1)) beam_energy = 50000 # keV beam_full = Beam(parent=world, transform=beam_transform) beam_full.plasma = plasma beam_full.atomic_data = adas beam_full.energy = beam_energy beam_full.power = 3e6 beam_full.element = deuterium beam_full.sigma = 0.05 beam_full.divergence_x = 0.5 beam_full.divergence_y = 0.5 beam_full.length = 3.0 beam_full.attenuator = SingleRayAttenuator(clamp_to_zero=True) beam_full.models = [BeamCXLine(Line(carbon, 5, (8, 7)))]
def __init__(self, pini_geometry, pini_parameters, plasma, atomic_data, attenuation_instructions, emission_instructions, integration_step=0.02, parent=None, name=""): source, direction, divergence, initial_width, length = pini_geometry energy, power_fractions, self._turned_on_func, element = pini_parameters self._components = [] self._length = length self._parent_reminder = parent # Rotation between 'direction' and the z unit vector # This is important because the beam primitives are defined along the z axis. self._origin = source self._direction = direction direction.normalise() rotation = rotate_basis(direction, Vector3D(0., 0., 1.)) transform_pini = translate(*source) * rotation Node.__init__(self, parent=parent, transform=transform_pini, name=name) attenuation_model_class, attenuation_model_arg = attenuation_instructions # the 3 energy components are different beams for comp_nb in [1, 2, 3]: # creation of the attenuation model # Note that each beamlet needs its own attenuation class instance. attenuation_model = attenuation_model_class( **attenuation_model_arg) # creation of the emission models emission_models = [] for (emission_model_class, argument_dictionary) in emission_instructions: emission_models.append( emission_model_class(**argument_dictionary)) beam = Beam(parent=self, transform=translate(0., 0., 0.), name="Beam component {}".format(comp_nb)) beam.plasma = plasma beam.atomic_data = atomic_data beam.energy = energy / comp_nb beam.power = power_fractions[comp_nb - 1] beam.element = element beam.sigma = initial_width beam.divergence_x = divergence[0] beam.divergence_y = divergence[1] beam.length = length beam.attenuator = attenuation_model beam.models = emission_models beam.integrator.step = integration_step beam.integrator.min_samples = 10 self._components.append(beam)
def load_kb1_camera(parent=None): camera_id = 'KB1' # Transforms, read from KB1 CAD model for INDIVIDUAL_BOLOMETER_ASSEMBLY # Note that the rotation angle is positive when Axis is the Z axis, and # negative when Axis is the -Z axis camera_transforms = [ translate(-1.73116, 2.59086, 3.31650) * rotate_z(123.75), translate(-3.05613, 0.60790, 3.31650) * rotate_z(168.75), translate(1.73116, -2.59086, 3.31650) * rotate_z(-56.25), translate(3.05613, -0.60790, 3.31650) * rotate_z(-11.25), ] # Transform for INDIVIDUAL_BOLOMETER_ASSEMBLY/SINGLE_BOLOMETER_ASSEMBLY/FOIL 1 # in CAD model foil_camera_transform = translate(0, 0, 18.70e-3) # Foils point downwards towards the plasma foil_orientation_transform = rotate_basis(Vector3D(0, 0, -1), Vector3D(0, 1, 0)) # Dimensions read from edge to edge (and adjacent vertices defining rounded corners) on # INDIVIDUAL_BOLOMETER_ASSEMBLY/SINGLE_BOLOMETER_ASSEMBLY/FOIL SUPPORT 1, # edges (and vertices) closest to the foil foil_width = 11e-3 foil_height = 11e-3 foil_curvature_radius = 1e-3 # KB1 does not really have a slit, per-se. The vessel functions as the # aperture. To ensure a sufficiently displaced bounding sphere for the # TargettedPixel, we'll put a dummy slit at the exit of the port through # which the camera views. Note that with the camera transform defined above, # the y axis is in the toroidal direction and the x axis in the inward # radial direction. # # The foil is not centred on the centre of the port. To measure the # displacement, the centre of the port was read from the CAD model for # KB1-1, then the vector from the foil centre to the centre of the port exit # for this channel was calculated in the foil's local coordinate system. foil_slit_transform = translate(-0.05025, 0, 1.38658) slit_width = 0.25 # slightly larger than widest point of port (~225 mm) slit_height = 0.09 # sligtly larger than length of port (~73.84 mm) num_slits = len(camera_transforms) num_foils = len(camera_transforms) bolometer_camera = BolometerCamera(name=camera_id, parent=parent) slit_objects = {} for i in range(num_slits): slit_id = '{}_Slit_#{}'.format(camera_id, i + 1) slit_transform = (camera_transforms[i] * foil_orientation_transform * foil_slit_transform * foil_camera_transform) centre_point = Point3D(0, 0, 0).transform(slit_transform) basis_x = Vector3D(1, 0, 0).transform(slit_transform) basis_y = Vector3D(0, 1, 0).transform(slit_transform) dx = slit_width dy = slit_height slit_objects[slit_id] = BolometerSlit(slit_id, centre_point, basis_x, dx, basis_y, dy, csg_aperture=True, parent=bolometer_camera) for i in range(num_foils): foil_id = '{}_CH{}_Foil'.format(camera_id, i + 1) slit_id = '{}_Slit_#{}'.format(camera_id, i + 1) foil_transform = (camera_transforms[i] * foil_orientation_transform * foil_camera_transform) centre_point = Point3D(0, 0, 0).transform(foil_transform) basis_x = Vector3D(1, 0, 0).transform(foil_transform) basis_y = Vector3D(0, 1, 0).transform(foil_transform) dx = foil_width dy = foil_height rc = foil_curvature_radius foil = BolometerFoil(foil_id, centre_point, basis_x, dx, basis_y, dy, slit_objects[slit_id], curvature_radius=rc, parent=bolometer_camera) bolometer_camera.add_foil_detector(foil) return bolometer_camera
pini_8_1 = load_pini_from_ppf(PULSE, '8.1', plasma, adas, attenuation_instructions, beam_emission_instructions, world) pini_8_2 = load_pini_from_ppf(PULSE, '8.2', plasma, adas, attenuation_instructions, beam_emission_instructions, world) pini_8_5 = load_pini_from_ppf(PULSE, '8.5', plasma, adas, attenuation_instructions, beam_emission_instructions, world) pini_8_6 = load_pini_from_ppf(PULSE, '8.6', plasma, adas, attenuation_instructions, beam_emission_instructions, world) # ############################### OBSERVATION ############################### # print('Observation') los = Point3D(4.22950, -0.791368, 0.269430) direction = Vector3D(-0.760612, -0.648906, -0.0197396).normalise() los = los + direction * 0.9 up = Vector3D(0, 0, 1) camera = PinholeCamera( (512, 512), fov=45, parent=world, transform=translate(los.x, los.y, los.z) * rotate_basis(direction, up)) camera.pixel_samples = 50 camera.spectral_bins = 15 camera.observe()
def raytraced_etendue(distance, detector_radius=0.001, ray_count=100000, batches=10): # generate the transform to the detector position and orientation detector_transform = translate(0, 0, distance) * rotate_basis(Vector3D(0, 0, -1), Vector3D(0, -1, 0)) # generate bounding sphere and convert to local coordinate system sphere = target.bounding_sphere() spheres = [(sphere.centre.transform(detector_transform), sphere.radius, 1.0)] # instance targetted pixel sampler targetted_sampler = TargettedHemisphereSampler(spheres) point_sampler = DiskSampler3D(detector_radius) detector_area = detector_radius**2 * np.pi solid_angle = 2 * np.pi etendue_sampled = solid_angle * detector_area etendues = [] for i in range(batches): # sample pixel origins origins = point_sampler(samples=ray_count) passed = 0.0 for origin in origins: # obtain targetted vector sample direction, pdf = targetted_sampler(origin, pdf=True) path_weight = R_2_PI * direction.z/pdf origin = origin.transform(detector_transform) direction = direction.transform(detector_transform) while True: # Find the next intersection point of the ray with the world intersection = world.hit(CoreRay(origin, direction)) if intersection is None: passed += 1 * path_weight break elif isinstance(intersection.primitive.material, NullMaterial): hit_point = intersection.hit_point.transform(intersection.primitive_to_world) origin = hit_point + direction * 1E-9 continue else: break if passed == 0: raise ValueError("Something is wrong with the scene-graph, calculated etendue should not zero.") etendue_fraction = passed / ray_count etendues.append(etendue_sampled * etendue_fraction) etendue = np.mean(etendues) etendue_error = np.std(etendues) return etendue, etendue_error
shift = translate(0, 0, -1) radiation_emitter = VolumeTransform(RadiationFunction(rad_function_3d), shift.inverse()) geom = Cylinder(CYLINDER_RADIUS, CYLINDER_HEIGHT, transform=shift, parent=world, material=radiation_emitter) ###################### # visualise emission # # run some plots to check the distribution functions and emission profile are as expected r, z, t_samples = sample2d(rad_function, (0, 4, 200), (-2, 2, 200)) plt.imshow(np.transpose(np.squeeze(t_samples)), extent=[0, 3, -1.5, 1.5]) plt.colorbar() plt.axis('equal') plt.xlabel('r axis') plt.ylabel('z axis') plt.title("Radiation profile in r-z plane") camera = PinholeCamera((256, 256), pipelines=[PowerPipeline2D()], parent=world) camera.transform = translate(-3.5, -1.5, 0) * rotate_basis( Vector3D(1, 0, 0), Vector3D(0, 0, 1)) camera.pixel_samples = 1 plt.ion() camera.observe() plt.ioff() plt.show()
def __init__(self, detector_id, centre_point, basis_x, dx, basis_y, dy, slit, parent=None, units="Power", accumulate=False, curvature_radius=0): # perform validation of input parameters if not isinstance(dx, (float, int)): raise TypeError("dx argument for BolometerFoil must be of type float/int.") if not dx > 0: raise ValueError("dx argument for BolometerFoil must be greater than zero.") if not isinstance(dy, (float, int)): raise TypeError("dy argument for BolometerFoil must be of type float/int.") if not dy > 0: raise ValueError("dy argument for BolometerFoil must be greater than zero.") if not isinstance(slit, BolometerSlit): raise TypeError("slit argument for BolometerFoil must be of type BolometerSlit.") if not isinstance(centre_point, Point3D): raise TypeError("centre_point argument for BolometerFoil must be of type Point3D.") if not isinstance(curvature_radius, (float, int)): raise TypeError("curvature_radius argument for BolometerFoil " "must be of type float/int.") if curvature_radius < 0: raise ValueError("curvature_radius argument for BolometerFoil " "must not be negative.") if not isinstance(basis_x, Vector3D): raise TypeError("The basis vectors of BolometerFoil must be of type Vector3D.") if not isinstance(basis_y, Vector3D): raise TypeError("The basis vectors of BolometerFoil must be of type Vector3D.") self._centre_point = centre_point self._basis_x = basis_x.normalise() self._basis_y = basis_y.normalise() self._normal_vec = self._basis_x.cross(self._basis_y) self._slit = slit self._foil_to_slit_vec = self._centre_point.vector_to(self._slit.centre_point).normalise() self._curvature_radius = curvature_radius self.units = units # setup root bolometer foil transform translation = translate(self._centre_point.x, self._centre_point.y, self._centre_point.z) rotation = rotate_basis(self._normal_vec, self._basis_y) if self.units == "Power": pipeline = PowerPipeline0D(accumulate=accumulate) elif self.units == "Radiance": pipeline = RadiancePipeline0D(accumulate=accumulate) else: raise ValueError("The units argument of BolometerFoil must be one of 'Power' or 'Radiance'.") super().__init__([slit.target], targetted_path_prob=1.0, pipelines=[pipeline], pixel_samples=1000, x_width=dx, y_width=dy, spectral_bins=1, quiet=True, parent=parent, transform=translation * rotation, name=detector_id) # round off the detector corners, if applicable if self._curvature_radius > 0: mask_corners(self)
for i, detector in enumerate(aug_wall_detectors): print() print("detector {}".format(i)) y_width = detector[2] centre_point = detector[3] normal_vector = detector[4] y_vector = detector[5] pixel_area = X_WIDTH * y_width power_data = PowerPipeline0D() pixel_transform = translate(centre_point.x, centre_point.y, centre_point.z) * rotate_basis( normal_vector, y_vector) pixel = Pixel([power_data], x_width=X_WIDTH, y_width=y_width, name='pixel-{}'.format(i), spectral_bins=1, transform=pixel_transform, parent=world, pixel_samples=500) pixel.observe() powers.append(power_data.value.mean / pixel_area) power_errors.append(power_data.value.error() / pixel_area) detector_numbers.append(i)
# unpack triangle 1 v1, v2, v3 = s1_triangles[s1_tri_id] u1x, u1y, u1z = s1_vertices[v1] u1 = Point3D(u1x, u1y, u1z) u2x, u2y, u2z = s1_vertices[v2] u2 = Point3D(u2x, u2y, u2z) u3x, u3y, u3z = s1_vertices[v3] u3 = Point3D(u3x, u3y, u3z) uc = Point3D((u1x + u2x + u3x) / 3, (u1y + u2y + u3y) / 3, (u1z + u2z + u3z) / 3) u1u2 = u1.vector_to(u2).normalise() u1u3 = u1.vector_to(u3).normalise() n_pi1 = u1u2.cross(u1u3).normalise() # normal vector of plane 1 # transform to x-y plane s1_transform = translate(u1.x, u1.y, u1.z) * rotate_basis(u1u2, n_pi1) s1_inv_transform = s1_transform.inverse() points = set() for vertex in (u1, u2, u3): tv = vertex.transform(s1_inv_transform) points.add((tv.x, tv.z)) debug_intersection_points = [] for intersection in s1_intersections[s1_tri_id]: s2_tri_id, ut1, ut2 = intersection debug_intersection_points.append((ut1, ut2)) ut1t = ut1.transform(s1_inv_transform)
plasma.integrator.min_samples = 1000 plasma.atomic_data = adas plasma.geometry = Cylinder(sigma * 2, sigma * 10.0) plasma.geometry_transform = translate(0, -sigma * 5.0, 0) * rotate(0, 90, 0) # # # ########################### NBI CONFIGURATION ############################# # #Geometry south_pos = Point3D(0.188819939, -6.88824321, 0.0) #Position of PINI grid center duct_pos = Point3D(0.539, -1.926, 0.00) #position of beam duct south_pos.vector_to(duct_pos) #beam vector beam_axis = south_pos.vector_to(duct_pos).normalise() up = Vector3D(0, 0, 1) beam_rotation = rotate_basis(beam_axis, up) beam_position = translate(south_pos.x, south_pos.y, south_pos.z) beam_full = Beam(parent=world, transform=beam_position * beam_rotation) beam_full.plasma = plasma beam_full.atomic_data = adas beam_full.energy = 65000 beam_full.power = 3e6 beam_full.element = elements.deuterium beam_full.sigma = 0.025 beam_full.divergence_x = 0 #0.5 beam_full.divergence_y = 0 #0.5 beam_full.length = 10.0 beam_full.attenuator = SingleRayAttenuator(clamp_to_zero=True) beam_full.models = [
def raytraced_etendue(distance, detector_radius=0.001, ray_count=100000, batches=10): # generate the transform to the detector position and orientation detector_transform = translate(0, 0, distance) * rotate_basis( Vector3D(0, 0, -1), Vector3D(0, -1, 0)) # generate bounding sphere and convert to local coordinate system sphere = target.bounding_sphere() spheres = [(sphere.centre.transform(detector_transform), sphere.radius, 1.0)] # instance targetted pixel sampler targetted_sampler = TargettedHemisphereSampler(spheres) point_sampler = DiskSampler3D(detector_radius) detector_area = detector_radius**2 * np.pi solid_angle = 2 * np.pi etendue_sampled = solid_angle * detector_area etendues = [] for i in range(batches): # sample pixel origins origins = point_sampler(samples=ray_count) passed = 0.0 for origin in origins: # obtain targetted vector sample direction, pdf = targetted_sampler(origin, pdf=True) path_weight = R_2_PI * direction.z / pdf origin = origin.transform(detector_transform) direction = direction.transform(detector_transform) while True: # Find the next intersection point of the ray with the world intersection = world.hit(CoreRay(origin, direction)) if intersection is None: passed += 1 * path_weight break elif isinstance(intersection.primitive.material, NullMaterial): hit_point = intersection.hit_point.transform( intersection.primitive_to_world) origin = hit_point + direction * 1E-9 continue else: break if passed == 0: raise ValueError( "Something is wrong with the scene-graph, calculated etendue should not zero." ) etendue_fraction = passed / ray_count etendues.append(etendue_sampled * etendue_fraction) etendue = np.mean(etendues) etendue_error = np.std(etendues) return etendue, etendue_error
visualise_scenegraph(world) input('pause...') world = World() Subtract(s1, b2, parent=world) visualise_scenegraph(world) input('pause...') ######################################################################################################################## # Cone and Cylinder c1 = Cone(0.15, 1) c2 = Cylinder(0.15, 0.5, transform=translate(-0.25, 0, 0.5) * rotate_basis(Vector3D(1, 0, 0), Vector3D(0, 0, 1))) world = World() Union(c1, c2, parent=world) visualise_scenegraph(world) input('pause...') world = World() Intersect(c1, c2, parent=world) visualise_scenegraph(world) input('pause...') world = World() Subtract(c1, c2, parent=world) visualise_scenegraph(world) input('pause...')