1// Copyright 2021 The Cirq Developers 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// https://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15import { 16 EllipseCurve, 17 BufferGeometry, 18 LineBasicMaterial, 19 Line, 20 Group, 21} from 'three'; 22import {Orientation} from './enums'; 23 24interface CurveData { 25 anchorX: number; 26 anchorY: number; 27 radius: number; 28 startAngle: number; 29 endAngle: number; 30 isClockwise: boolean; 31 rotation: number; 32} 33 34/** 35 * Generates the meridians for the Bloch sphere. The radius, 36 * number of meridian circles, and the orientation of the circles are configurable. 37 */ 38export class Meridians extends Group { 39 readonly radius: number; 40 readonly numCircles: number; 41 readonly orientation: Orientation; 42 readonly color: string = 'gray'; 43 44 /** 45 * Class constructor. 46 * @param radius The radius of the Bloch Sphere meridians. This should be equal 47 * to the radius of the sphere instance. 48 * @param numCircles The number of circles desired for the given set of meridians. 49 * Note that certain types of meridians have certain guidelines to which numbers are possible 50 * @param orientation The orientation of the meridians (HORIZONTAL_CHORD, HORIZONTAL, VERTICAL) 51 * @returns An instance of the class including generated meridians. This can be 52 * added to the Bloch sphere instance as well as the scene. 53 */ 54 constructor(radius: number, numCircles: number, orientation: Orientation) { 55 super(); 56 this.radius = radius; 57 this.orientation = orientation; 58 59 switch (orientation) { 60 case Orientation.HORIZONTAL: { 61 this.numCircles = this.sanitizeCircleInput(numCircles); 62 this.createHorizontalChordMeridians(this.radius, this.numCircles); 63 return this; 64 } 65 case Orientation.VERTICAL: { 66 this.numCircles = this.sanitizeCircleInput(numCircles); 67 this.createVerticalMeridians(this.radius, this.numCircles); 68 return this; 69 } 70 default: 71 throw new Error('Invalid orientation input in Meridians constructor'); 72 } 73 } 74 75 /** 76 * Creates the special horizontal meridian lines of the Bloch 77 * sphere, each with a different radius and location, adding 78 * them to the group afterwards. 79 * @param radius The radius of the overall Bloch sphere 80 * @param numCircles The number of circles displayed. The number must be odd, 81 * if an even number is provided. it will round up to the next highest odd number. 82 * If 0 < numCircles < 3, 3 meridians will be displayed. 83 */ 84 private createHorizontalChordMeridians(radius: number, numCircles: number) { 85 if (numCircles === 0) { 86 return; 87 } 88 89 let nonEquatorCircles: number; 90 numCircles % 2 !== 0 91 ? (nonEquatorCircles = numCircles - 1) 92 : (nonEquatorCircles = numCircles); 93 const circlesPerHalf = nonEquatorCircles / 2; 94 95 // Creates chords proportionally to radius 5 circle. 96 const initialFactor = (0.5 * radius) / 5; 97 98 // Start building the chords from the top down. 99 // If the user only wants one chord, skip the loop. 100 let topmostChordPos: number; 101 numCircles === 1 102 ? (topmostChordPos = 0) 103 : (topmostChordPos = radius - initialFactor); 104 105 const chordYPositions = [0]; // equator 106 for ( 107 let i = topmostChordPos; 108 i > 0; 109 i -= topmostChordPos / circlesPerHalf 110 ) { 111 chordYPositions.push(i); 112 chordYPositions.push(-i); 113 } 114 115 // Calculate the lengths of the chords of the circle, and then add them 116 for (const position of chordYPositions) { 117 const hyp2 = Math.pow(radius, 2); 118 const distance2 = Math.pow(position, 2); 119 const newRadius = Math.sqrt(hyp2 - distance2); // radius^2 - b^2 = a^2 120 121 const curveData = this.curveDataWithRadius(newRadius); 122 const curve = this.createMeridianCurve(curveData); 123 const meridianLine = this.createMeridianLine( 124 curve, 125 Math.PI / 2, 126 Orientation.HORIZONTAL, 127 position 128 ); 129 this.add(meridianLine); 130 } 131 } 132 133 /** 134 * Creates equally spaced and sized vertical meridian lines which rotate 135 * by varying degrees across the same axis, adding them to the group. 136 * @param radius The radius of the overall bloch sphere 137 * @param numCircles The number of circles to add. This number must be even, 138 * if an odd number is provided, one more circle will be generated to ensure it is even. 139 */ 140 private createVerticalMeridians(radius: number, numCircles: number) { 141 if (numCircles === 0) { 142 return; 143 } 144 145 const curveData = { 146 anchorX: 0, 147 anchorY: 0, 148 radius, 149 startAngle: 0, 150 endAngle: 2 * Math.PI, 151 isClockwise: false, 152 rotation: 0, 153 }; 154 155 for (let i = 0; i < Math.PI; i += Math.PI / numCircles) { 156 const curve = this.createMeridianCurve(curveData); 157 const meridianLine = this.createMeridianLine( 158 curve, 159 i, 160 Orientation.VERTICAL 161 ); 162 this.add(meridianLine); 163 } 164 } 165 166 /** 167 * Helper function that generates the actual Line object which will be 168 * rendered by the three.js scene. 169 * @param curve An EllipseCurve object that provides location/size info 170 * @param rotationAngle The desired angle of rotation in radians 171 * @param orientation The orientation of the meridian (horizontal_chord, horizontal or vertical) 172 * @param yPosition (Optional) Allows the yPosition of the line to be updated to 173 * the provided value 174 * @returns A Line object that can be rendered by a three.js scene. 175 */ 176 private createMeridianLine( 177 curve: EllipseCurve, 178 rotationAngle: number, 179 orientation: Orientation, 180 yPosition?: number 181 ): Line { 182 const points = curve.getSpacedPoints(128); 183 const meridianGeom = new BufferGeometry().setFromPoints(points); 184 185 switch (orientation) { 186 case Orientation.HORIZONTAL: { 187 meridianGeom.rotateX(rotationAngle); 188 break; 189 } 190 case Orientation.VERTICAL: { 191 meridianGeom.rotateY(rotationAngle); 192 break; 193 } 194 // No default case is needed, since an invalid orientation input will never make it 195 // through the constructor. 196 } 197 198 const meridianLine = new Line( 199 meridianGeom, 200 new LineBasicMaterial({color: 'gray'}) 201 ); 202 if (yPosition) { 203 meridianLine.position.y = yPosition; 204 } 205 return meridianLine; 206 } 207 208 /** 209 * Helper function that generates a necessary EllipseCurve 210 * given the required information. 211 * @param curveData An object that contains info about the curve 212 * @returns An EllipseCurve object based off the curve information. 213 */ 214 private createMeridianCurve(curveData: CurveData): EllipseCurve { 215 return new EllipseCurve( 216 curveData.anchorX, 217 curveData.anchorY, 218 curveData.radius, 219 curveData.radius, 220 curveData.startAngle, 221 curveData.endAngle, 222 curveData.isClockwise, 223 curveData.rotation 224 ); 225 } 226 227 private curveDataWithRadius(radius: number): CurveData { 228 return { 229 anchorX: 0, 230 anchorY: 0, 231 radius, 232 startAngle: 0, 233 endAngle: 2 * Math.PI, 234 isClockwise: false, 235 rotation: 0, 236 }; 237 } 238 239 private sanitizeCircleInput(input: number) { 240 if (input < 0) { 241 throw new Error('A negative number of meridians are not supported'); 242 } else if (input > 300) { 243 throw new Error('Over 300 meridians are not supported'); 244 } 245 246 return Math.floor(input); 247 } 248} 249