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