diff --git a/doc/python_api_reference_vDev.md b/doc/python_api_reference_vDev.md index 145d04ac..4ba62593 100644 --- a/doc/python_api_reference_vDev.md +++ b/doc/python_api_reference_vDev.md @@ -186,6 +186,7 @@ API references for stable versions are kept on the [stim github wiki](https://gi - [`stim.FlipSimulator.__init__`](#stim.FlipSimulator.__init__) - [`stim.FlipSimulator.batch_size`](#stim.FlipSimulator.batch_size) - [`stim.FlipSimulator.broadcast_pauli_errors`](#stim.FlipSimulator.broadcast_pauli_errors) + - [`stim.FlipSimulator.copy`](#stim.FlipSimulator.copy) - [`stim.FlipSimulator.do`](#stim.FlipSimulator.do) - [`stim.FlipSimulator.get_detector_flips`](#stim.FlipSimulator.get_detector_flips) - [`stim.FlipSimulator.get_measurement_flips`](#stim.FlipSimulator.get_measurement_flips) @@ -195,6 +196,7 @@ API references for stable versions are kept on the [stim github wiki](https://gi - [`stim.FlipSimulator.num_observables`](#stim.FlipSimulator.num_observables) - [`stim.FlipSimulator.num_qubits`](#stim.FlipSimulator.num_qubits) - [`stim.FlipSimulator.peek_pauli_flips`](#stim.FlipSimulator.peek_pauli_flips) + - [`stim.FlipSimulator.reset`](#stim.FlipSimulator.reset) - [`stim.FlipSimulator.set_pauli_flip`](#stim.FlipSimulator.set_pauli_flip) - [`stim.FlippedMeasurement`](#stim.FlippedMeasurement) - [`stim.FlippedMeasurement.__init__`](#stim.FlippedMeasurement.__init__) @@ -1680,11 +1682,19 @@ def diagram( Passing `range(A, B)` for a time slice will show the operations between tick A and tick B. - filter_coords: A set of acceptable coordinate prefixes, or - desired stim.DemTargets. For detector slice diagrams, only - detectors match one of the filters are included. If no filter - is specified, all detectors are included (but no observables). - To include an observable, add it as one of the filters. + rows: In diagrams that have multiple separate pieces, such as timeslice + diagrams and detslice diagrams, this controls how many rows of + pieces there will be. If not specified, a number of rows that creates + a roughly square layout will be chosen. + filter_coords: A list of things to include in the diagram. Different + effects depending on the diagram. + + For detslice diagrams, the filter defaults to showing all detectors + and no observables. When specified, each list entry can be a collection + of floats (detectors whose coordinates start with the same numbers will + be included), a stim.DemTarget (specifying a detector or observable + to include), a string like "D5" or "L0" specifying a detector or + observable to include. Returns: An object whose `__str__` method returns the diagram, so that @@ -5304,7 +5314,24 @@ def __str__( def args_copy( self, ) -> List[float]: - """Returns a copy of the list of numbers parameterizing the instruction (e.g. the probability of an error). + """Returns a copy of the list of numbers parameterizing the instruction. + + For example, this would be coordinates of a detector instruction or the + probability of an error instruction. The result is a copy, meaning that + editing it won't change the instruction's targets or future copies. + + Examples: + >>> import stim + >>> instruction = stim.DetectorErrorModel(''' + ... error(0.125) D0 + ... ''')[0] + >>> instruction.args_copy() + [0.125] + + >>> instruction.args_copy() == instruction.args_copy() + True + >>> instruction.args_copy() is instruction.args_copy() + False """ ``` @@ -5318,8 +5345,21 @@ def targets_copy( ) -> List[Union[int, stim.DemTarget]]: """Returns a copy of the instruction's targets. - (Making a copy is enforced to make it clear that editing the result won't change - the instruction's targets.) + The result is a copy, meaning that editing it won't change the instruction's + targets or future copies. + + Examples: + >>> import stim + >>> instruction = stim.DetectorErrorModel(''' + ... error(0.125) D0 L2 + ... ''')[0] + >>> instruction.targets_copy() + [stim.DemTarget('D0'), stim.DemTarget('L2')] + + >>> instruction.targets_copy() == instruction.targets_copy() + True + >>> instruction.targets_copy() is instruction.targets_copy() + False """ ``` @@ -7177,6 +7217,83 @@ def broadcast_pauli_errors( """ ``` + +```python +# stim.FlipSimulator.copy + +# (in class stim.FlipSimulator) +def copy( + self, + *, + copy_rng: bool = False, + seed: Optional[int] = None, +) -> stim.FlipSimulator: + """Returns a simulator with the same internal state, except perhaps its prng. + + Args: + copy_rng: Defaults to False. When False, the copy's pseudo random number + generator is reinitialized with a random seed instead of being a copy + of the original simulator's pseudo random number generator. This + causes the copy and the original to sample independent randomness, + instead of identical randomness, for future random operations. When set + to true, the copy will have the exact same pseudo random number + generator state as the original, and so will produce identical results + if told to do the same noisy operations. This argument is incompatible + with the `seed` argument. + + seed: PARTIALLY determines simulation results by deterministically seeding + the random number generator. + + Must be None or an integer in range(2**64). + + Defaults to None. When None, the prng state is either copied from the + original simulator or reseeded from system entropy, depending on the + copy_rng argument. + + When set to an integer, making the exact same series calls on the exact + same machine with the exact same version of Stim will produce the exact + same simulation results. + + CAUTION: simulation results *WILL NOT* be consistent between versions of + Stim. This restriction is present to make it possible to have future + optimizations to the random sampling, and is enforced by introducing + intentional differences in the seeding strategy from version to version. + + CAUTION: simulation results *MAY NOT* be consistent across machines that + differ in the width of supported SIMD instructions. For example, using + the same seed on a machine that supports AVX instructions and one that + only supports SSE instructions may produce different simulation results. + + CAUTION: simulation results *MAY NOT* be consistent if you vary how the + circuit is executed. For example, reordering whether a reset on one + qubit happens before or after a reset on another qubit can result in + different measurement results being observed starting from the same + seed. + + Returns: + The copy of the simulator. + + Examples: + >>> import stim + >>> import numpy as np + + >>> s1 = stim.FlipSimulator(batch_size=256) + >>> s1.set_pauli_flip('X', qubit_index=2, instance_index=3) + >>> s2 = s1.copy() + >>> s2 is s1 + False + >>> s2.peek_pauli_flips() == s1.peek_pauli_flips() + True + + >>> s1 = stim.FlipSimulator(batch_size=256) + >>> s2 = s1.copy(copy_rng=True) + >>> s1.do(stim.Circuit("X_ERROR(0.25) 0 \n M 0")) + >>> s2.do(stim.Circuit("X_ERROR(0.25) 0 \n M 0")) + >>> np.array_equal(s1.get_measurement_flips(), s2.get_measurement_flips()) + True + """ +``` + ```python # stim.FlipSimulator.do @@ -7613,6 +7730,38 @@ def peek_pauli_flips( """ ``` + +```python +# stim.FlipSimulator.reset + +# (in class stim.FlipSimulator) +def reset( + self, +) -> None: + """Resets the simulator's state, so it can be reused for another simulation. + + This empties the measurement flip history, empties the detector flip history, + and zeroes the observable flip state. It also resets all qubits to |0>. If + stabilizer randomization is disabled, this zeros all pauli flips data. Otherwise + it randomizes all pauli flips to be I or Z with equal probability. + + Examples: + >>> import stim + >>> sim = stim.FlipSimulator(batch_size=256) + >>> sim.do(stim.Circuit("M(0.1) 9")) + >>> sim.num_qubits + 10 + >>> sim.get_measurement_flips().shape + (1, 256) + + >>> sim.reset() + >>> sim.num_qubits + 10 + >>> sim.get_measurement_flips().shape + (0, 256) + """ +``` + ```python # stim.FlipSimulator.set_pauli_flip @@ -9533,6 +9682,13 @@ def __len__( self, ) -> int: """Returns the length the pauli string; the number of qubits it operates on. + + Examples: + >>> import stim + >>> len(stim.PauliString("XY_ZZ")) + 5 + >>> len(stim.PauliString("X0*Z99")) + 100 """ ``` @@ -10663,6 +10819,12 @@ def __len__( self, ) -> int: """Returns the number of qubits operated on by the tableau. + + Examples: + >>> import stim + >>> t = stim.Tableau.from_named_gate("CNOT") + >>> len(t) + 2 """ ``` diff --git a/doc/stim.pyi b/doc/stim.pyi index bbd39402..cd232b6a 100644 --- a/doc/stim.pyi +++ b/doc/stim.pyi @@ -1084,11 +1084,19 @@ class Circuit: Passing `range(A, B)` for a time slice will show the operations between tick A and tick B. - filter_coords: A set of acceptable coordinate prefixes, or - desired stim.DemTargets. For detector slice diagrams, only - detectors match one of the filters are included. If no filter - is specified, all detectors are included (but no observables). - To include an observable, add it as one of the filters. + rows: In diagrams that have multiple separate pieces, such as timeslice + diagrams and detslice diagrams, this controls how many rows of + pieces there will be. If not specified, a number of rows that creates + a roughly square layout will be chosen. + filter_coords: A list of things to include in the diagram. Different + effects depending on the diagram. + + For detslice diagrams, the filter defaults to showing all detectors + and no observables. When specified, each list entry can be a collection + of floats (detectors whose coordinates start with the same numbers will + be included), a stim.DemTarget (specifying a detector or observable + to include), a string like "D5" or "L0" specifying a detector or + observable to include. Returns: An object whose `__str__` method returns the diagram, so that @@ -4145,15 +4153,45 @@ class DemInstruction: def args_copy( self, ) -> List[float]: - """Returns a copy of the list of numbers parameterizing the instruction (e.g. the probability of an error). + """Returns a copy of the list of numbers parameterizing the instruction. + + For example, this would be coordinates of a detector instruction or the + probability of an error instruction. The result is a copy, meaning that + editing it won't change the instruction's targets or future copies. + + Examples: + >>> import stim + >>> instruction = stim.DetectorErrorModel(''' + ... error(0.125) D0 + ... ''')[0] + >>> instruction.args_copy() + [0.125] + + >>> instruction.args_copy() == instruction.args_copy() + True + >>> instruction.args_copy() is instruction.args_copy() + False """ def targets_copy( self, ) -> List[Union[int, stim.DemTarget]]: """Returns a copy of the instruction's targets. - (Making a copy is enforced to make it clear that editing the result won't change - the instruction's targets.) + The result is a copy, meaning that editing it won't change the instruction's + targets or future copies. + + Examples: + >>> import stim + >>> instruction = stim.DetectorErrorModel(''' + ... error(0.125) D0 L2 + ... ''')[0] + >>> instruction.targets_copy() + [stim.DemTarget('D0'), stim.DemTarget('L2')] + + >>> instruction.targets_copy() == instruction.targets_copy() + True + >>> instruction.targets_copy() is instruction.targets_copy() + False """ @property def type( @@ -5575,6 +5613,76 @@ class FlipSimulator: >>> sim.peek_pauli_flips() [stim.PauliString("+X_Y"), stim.PauliString("+Z_Y")] """ + def copy( + self, + *, + copy_rng: bool = False, + seed: Optional[int] = None, + ) -> stim.FlipSimulator: + """Returns a simulator with the same internal state, except perhaps its prng. + + Args: + copy_rng: Defaults to False. When False, the copy's pseudo random number + generator is reinitialized with a random seed instead of being a copy + of the original simulator's pseudo random number generator. This + causes the copy and the original to sample independent randomness, + instead of identical randomness, for future random operations. When set + to true, the copy will have the exact same pseudo random number + generator state as the original, and so will produce identical results + if told to do the same noisy operations. This argument is incompatible + with the `seed` argument. + + seed: PARTIALLY determines simulation results by deterministically seeding + the random number generator. + + Must be None or an integer in range(2**64). + + Defaults to None. When None, the prng state is either copied from the + original simulator or reseeded from system entropy, depending on the + copy_rng argument. + + When set to an integer, making the exact same series calls on the exact + same machine with the exact same version of Stim will produce the exact + same simulation results. + + CAUTION: simulation results *WILL NOT* be consistent between versions of + Stim. This restriction is present to make it possible to have future + optimizations to the random sampling, and is enforced by introducing + intentional differences in the seeding strategy from version to version. + + CAUTION: simulation results *MAY NOT* be consistent across machines that + differ in the width of supported SIMD instructions. For example, using + the same seed on a machine that supports AVX instructions and one that + only supports SSE instructions may produce different simulation results. + + CAUTION: simulation results *MAY NOT* be consistent if you vary how the + circuit is executed. For example, reordering whether a reset on one + qubit happens before or after a reset on another qubit can result in + different measurement results being observed starting from the same + seed. + + Returns: + The copy of the simulator. + + Examples: + >>> import stim + >>> import numpy as np + + >>> s1 = stim.FlipSimulator(batch_size=256) + >>> s1.set_pauli_flip('X', qubit_index=2, instance_index=3) + >>> s2 = s1.copy() + >>> s2 is s1 + False + >>> s2.peek_pauli_flips() == s1.peek_pauli_flips() + True + + >>> s1 = stim.FlipSimulator(batch_size=256) + >>> s2 = s1.copy(copy_rng=True) + >>> s1.do(stim.Circuit("X_ERROR(0.25) 0 \n M 0")) + >>> s2.do(stim.Circuit("X_ERROR(0.25) 0 \n M 0")) + >>> np.array_equal(s1.get_measurement_flips(), s2.get_measurement_flips()) + True + """ def do( self, obj: Union[stim.Circuit, stim.CircuitInstruction, stim.CircuitRepeatBlock], @@ -5948,6 +6056,31 @@ class FlipSimulator: >>> sorted(set(str(flips))) # Should have Zs from stabilizer randomization ['+', 'Z', '_'] """ + def reset( + self, + ) -> None: + """Resets the simulator's state, so it can be reused for another simulation. + + This empties the measurement flip history, empties the detector flip history, + and zeroes the observable flip state. It also resets all qubits to |0>. If + stabilizer randomization is disabled, this zeros all pauli flips data. Otherwise + it randomizes all pauli flips to be I or Z with equal probability. + + Examples: + >>> import stim + >>> sim = stim.FlipSimulator(batch_size=256) + >>> sim.do(stim.Circuit("M(0.1) 9")) + >>> sim.num_qubits + 10 + >>> sim.get_measurement_flips().shape + (1, 256) + + >>> sim.reset() + >>> sim.num_qubits + 10 + >>> sim.get_measurement_flips().shape + (0, 256) + """ def set_pauli_flip( self, pauli: Union[str, int], @@ -7401,6 +7534,13 @@ class PauliString: self, ) -> int: """Returns the length the pauli string; the number of qubits it operates on. + + Examples: + >>> import stim + >>> len(stim.PauliString("XY_ZZ")) + 5 + >>> len(stim.PauliString("X0*Z99")) + 100 """ def __mul__( self, @@ -8306,6 +8446,12 @@ class Tableau: self, ) -> int: """Returns the number of qubits operated on by the tableau. + + Examples: + >>> import stim + >>> t = stim.Tableau.from_named_gate("CNOT") + >>> len(t) + 2 """ def __mul__( self, diff --git a/glue/python/src/stim/__init__.pyi b/glue/python/src/stim/__init__.pyi index bbd39402..cd232b6a 100644 --- a/glue/python/src/stim/__init__.pyi +++ b/glue/python/src/stim/__init__.pyi @@ -1084,11 +1084,19 @@ class Circuit: Passing `range(A, B)` for a time slice will show the operations between tick A and tick B. - filter_coords: A set of acceptable coordinate prefixes, or - desired stim.DemTargets. For detector slice diagrams, only - detectors match one of the filters are included. If no filter - is specified, all detectors are included (but no observables). - To include an observable, add it as one of the filters. + rows: In diagrams that have multiple separate pieces, such as timeslice + diagrams and detslice diagrams, this controls how many rows of + pieces there will be. If not specified, a number of rows that creates + a roughly square layout will be chosen. + filter_coords: A list of things to include in the diagram. Different + effects depending on the diagram. + + For detslice diagrams, the filter defaults to showing all detectors + and no observables. When specified, each list entry can be a collection + of floats (detectors whose coordinates start with the same numbers will + be included), a stim.DemTarget (specifying a detector or observable + to include), a string like "D5" or "L0" specifying a detector or + observable to include. Returns: An object whose `__str__` method returns the diagram, so that @@ -4145,15 +4153,45 @@ class DemInstruction: def args_copy( self, ) -> List[float]: - """Returns a copy of the list of numbers parameterizing the instruction (e.g. the probability of an error). + """Returns a copy of the list of numbers parameterizing the instruction. + + For example, this would be coordinates of a detector instruction or the + probability of an error instruction. The result is a copy, meaning that + editing it won't change the instruction's targets or future copies. + + Examples: + >>> import stim + >>> instruction = stim.DetectorErrorModel(''' + ... error(0.125) D0 + ... ''')[0] + >>> instruction.args_copy() + [0.125] + + >>> instruction.args_copy() == instruction.args_copy() + True + >>> instruction.args_copy() is instruction.args_copy() + False """ def targets_copy( self, ) -> List[Union[int, stim.DemTarget]]: """Returns a copy of the instruction's targets. - (Making a copy is enforced to make it clear that editing the result won't change - the instruction's targets.) + The result is a copy, meaning that editing it won't change the instruction's + targets or future copies. + + Examples: + >>> import stim + >>> instruction = stim.DetectorErrorModel(''' + ... error(0.125) D0 L2 + ... ''')[0] + >>> instruction.targets_copy() + [stim.DemTarget('D0'), stim.DemTarget('L2')] + + >>> instruction.targets_copy() == instruction.targets_copy() + True + >>> instruction.targets_copy() is instruction.targets_copy() + False """ @property def type( @@ -5575,6 +5613,76 @@ class FlipSimulator: >>> sim.peek_pauli_flips() [stim.PauliString("+X_Y"), stim.PauliString("+Z_Y")] """ + def copy( + self, + *, + copy_rng: bool = False, + seed: Optional[int] = None, + ) -> stim.FlipSimulator: + """Returns a simulator with the same internal state, except perhaps its prng. + + Args: + copy_rng: Defaults to False. When False, the copy's pseudo random number + generator is reinitialized with a random seed instead of being a copy + of the original simulator's pseudo random number generator. This + causes the copy and the original to sample independent randomness, + instead of identical randomness, for future random operations. When set + to true, the copy will have the exact same pseudo random number + generator state as the original, and so will produce identical results + if told to do the same noisy operations. This argument is incompatible + with the `seed` argument. + + seed: PARTIALLY determines simulation results by deterministically seeding + the random number generator. + + Must be None or an integer in range(2**64). + + Defaults to None. When None, the prng state is either copied from the + original simulator or reseeded from system entropy, depending on the + copy_rng argument. + + When set to an integer, making the exact same series calls on the exact + same machine with the exact same version of Stim will produce the exact + same simulation results. + + CAUTION: simulation results *WILL NOT* be consistent between versions of + Stim. This restriction is present to make it possible to have future + optimizations to the random sampling, and is enforced by introducing + intentional differences in the seeding strategy from version to version. + + CAUTION: simulation results *MAY NOT* be consistent across machines that + differ in the width of supported SIMD instructions. For example, using + the same seed on a machine that supports AVX instructions and one that + only supports SSE instructions may produce different simulation results. + + CAUTION: simulation results *MAY NOT* be consistent if you vary how the + circuit is executed. For example, reordering whether a reset on one + qubit happens before or after a reset on another qubit can result in + different measurement results being observed starting from the same + seed. + + Returns: + The copy of the simulator. + + Examples: + >>> import stim + >>> import numpy as np + + >>> s1 = stim.FlipSimulator(batch_size=256) + >>> s1.set_pauli_flip('X', qubit_index=2, instance_index=3) + >>> s2 = s1.copy() + >>> s2 is s1 + False + >>> s2.peek_pauli_flips() == s1.peek_pauli_flips() + True + + >>> s1 = stim.FlipSimulator(batch_size=256) + >>> s2 = s1.copy(copy_rng=True) + >>> s1.do(stim.Circuit("X_ERROR(0.25) 0 \n M 0")) + >>> s2.do(stim.Circuit("X_ERROR(0.25) 0 \n M 0")) + >>> np.array_equal(s1.get_measurement_flips(), s2.get_measurement_flips()) + True + """ def do( self, obj: Union[stim.Circuit, stim.CircuitInstruction, stim.CircuitRepeatBlock], @@ -5948,6 +6056,31 @@ class FlipSimulator: >>> sorted(set(str(flips))) # Should have Zs from stabilizer randomization ['+', 'Z', '_'] """ + def reset( + self, + ) -> None: + """Resets the simulator's state, so it can be reused for another simulation. + + This empties the measurement flip history, empties the detector flip history, + and zeroes the observable flip state. It also resets all qubits to |0>. If + stabilizer randomization is disabled, this zeros all pauli flips data. Otherwise + it randomizes all pauli flips to be I or Z with equal probability. + + Examples: + >>> import stim + >>> sim = stim.FlipSimulator(batch_size=256) + >>> sim.do(stim.Circuit("M(0.1) 9")) + >>> sim.num_qubits + 10 + >>> sim.get_measurement_flips().shape + (1, 256) + + >>> sim.reset() + >>> sim.num_qubits + 10 + >>> sim.get_measurement_flips().shape + (0, 256) + """ def set_pauli_flip( self, pauli: Union[str, int], @@ -7401,6 +7534,13 @@ class PauliString: self, ) -> int: """Returns the length the pauli string; the number of qubits it operates on. + + Examples: + >>> import stim + >>> len(stim.PauliString("XY_ZZ")) + 5 + >>> len(stim.PauliString("X0*Z99")) + 100 """ def __mul__( self, @@ -8306,6 +8446,12 @@ class Tableau: self, ) -> int: """Returns the number of qubits operated on by the tableau. + + Examples: + >>> import stim + >>> t = stim.Tableau.from_named_gate("CNOT") + >>> len(t) + 2 """ def __mul__( self, diff --git a/src/stim/circuit/circuit.pybind.cc b/src/stim/circuit/circuit.pybind.cc index 4c870e61..8c801e5c 100644 --- a/src/stim/circuit/circuit.pybind.cc +++ b/src/stim/circuit/circuit.pybind.cc @@ -3267,6 +3267,7 @@ void stim_pybind::pybind_circuit_methods(pybind11::module &, pybind11::class_ 'stim._DiagramHelper': @@ -3343,11 +3344,19 @@ void stim_pybind::pybind_circuit_methods(pybind11::module &, pybind11::class_ filter_coords; try { @@ -221,6 +222,11 @@ DiagramHelper stim_pybind::circuit_diagram( throw std::invalid_argument("filter_coords wasn't an Iterable[stim.DemTarget | Iterable[float]]."); } + size_t num_rows = 0; + if (!rows.is_none()) { + num_rows = pybind11::cast(rows); + } + uint64_t tick_min; uint64_t num_ticks; if (tick.is_none()) { @@ -262,7 +268,7 @@ DiagramHelper stim_pybind::circuit_diagram( type == "timeslice" || type == "time-slice") { std::stringstream out; DiagramTimelineSvgDrawer::make_diagram_write_to( - circuit, out, tick_min, num_ticks, DiagramTimelineSvgDrawerMode::SVG_MODE_TIME_SLICE, filter_coords); + circuit, out, tick_min, num_ticks, DiagramTimelineSvgDrawerMode::SVG_MODE_TIME_SLICE, filter_coords, num_rows); DiagramType d_type = type.find("html") != std::string::npos ? DiagramType::DIAGRAM_TYPE_SVG_HTML : DiagramType::DIAGRAM_TYPE_SVG; return DiagramHelper{d_type, out.str()}; @@ -270,7 +276,7 @@ DiagramHelper stim_pybind::circuit_diagram( type == "detslice-svg" || type == "detslice" || type == "detslice-html" || type == "detslice-svg-html" || type == "detector-slice-svg" || type == "detector-slice") { std::stringstream out; - DetectorSliceSet::from_circuit_ticks(circuit, tick_min, num_ticks, filter_coords).write_svg_diagram_to(out); + DetectorSliceSet::from_circuit_ticks(circuit, tick_min, num_ticks, filter_coords).write_svg_diagram_to(out, num_rows); DiagramType d_type = type.find("html") != std::string::npos ? DiagramType::DIAGRAM_TYPE_SVG_HTML : DiagramType::DIAGRAM_TYPE_SVG; return DiagramHelper{d_type, out.str()}; @@ -284,7 +290,8 @@ DiagramHelper stim_pybind::circuit_diagram( tick_min, num_ticks, DiagramTimelineSvgDrawerMode::SVG_MODE_TIME_DETECTOR_SLICE, - filter_coords); + filter_coords, + num_rows); DiagramType d_type = type.find("html") != std::string::npos ? DiagramType::DIAGRAM_TYPE_SVG_HTML : DiagramType::DIAGRAM_TYPE_SVG; return DiagramHelper{d_type, out.str()}; diff --git a/src/stim/cmd/command_diagram.pybind.h b/src/stim/cmd/command_diagram.pybind.h index 9e38276f..7c25a628 100644 --- a/src/stim/cmd/command_diagram.pybind.h +++ b/src/stim/cmd/command_diagram.pybind.h @@ -42,6 +42,7 @@ DiagramHelper circuit_diagram( const stim::Circuit &circuit, std::string_view type, const pybind11::object &tick, + const pybind11::object &rows, const pybind11::object &filter_coords_obj); } // namespace stim_pybind diff --git a/src/stim/dem/detector_error_model_instruction.pybind.cc b/src/stim/dem/detector_error_model_instruction.pybind.cc index dfd98350..a9f0a5ca 100644 --- a/src/stim/dem/detector_error_model_instruction.pybind.cc +++ b/src/stim/dem/detector_error_model_instruction.pybind.cc @@ -14,7 +14,6 @@ #include "stim/dem/detector_error_model_instruction.pybind.h" -#include "stim/dem/detector_error_model.pybind.h" #include "stim/dem/detector_error_model_target.pybind.h" #include "stim/py/base.pybind.h" #include "stim/util_bot/str_util.h" @@ -183,7 +182,29 @@ void stim_pybind::pybind_detector_error_model_instruction_methods( c.def( "args_copy", &ExposedDemInstruction::args_copy, - "Returns a copy of the list of numbers parameterizing the instruction (e.g. the probability of an error)."); + clean_doc_string(R"DOC( + @signature def args_copy(self) -> List[float]: + Returns a copy of the list of numbers parameterizing the instruction. + + For example, this would be coordinates of a detector instruction or the + probability of an error instruction. The result is a copy, meaning that + editing it won't change the instruction's targets or future copies. + + Examples: + >>> import stim + >>> instruction = stim.DetectorErrorModel(''' + ... error(0.125) D0 + ... ''')[0] + >>> instruction.args_copy() + [0.125] + + >>> instruction.args_copy() == instruction.args_copy() + True + >>> instruction.args_copy() is instruction.args_copy() + False + )DOC") + .data()); + c.def( "targets_copy", &ExposedDemInstruction::targets_copy, @@ -191,8 +212,21 @@ void stim_pybind::pybind_detector_error_model_instruction_methods( @signature def targets_copy(self) -> List[Union[int, stim.DemTarget]]: Returns a copy of the instruction's targets. - (Making a copy is enforced to make it clear that editing the result won't change - the instruction's targets.) + The result is a copy, meaning that editing it won't change the instruction's + targets or future copies. + + Examples: + >>> import stim + >>> instruction = stim.DetectorErrorModel(''' + ... error(0.125) D0 L2 + ... ''')[0] + >>> instruction.targets_copy() + [stim.DemTarget('D0'), stim.DemTarget('L2')] + + >>> instruction.targets_copy() == instruction.targets_copy() + True + >>> instruction.targets_copy() is instruction.targets_copy() + False )DOC") .data()); c.def_property_readonly( diff --git a/src/stim/diagram/detector_slice/detector_slice_set.cc b/src/stim/diagram/detector_slice/detector_slice_set.cc index abf26b06..ad90760c 100644 --- a/src/stim/diagram/detector_slice/detector_slice_set.cc +++ b/src/stim/diagram/detector_slice/detector_slice_set.cc @@ -741,8 +741,7 @@ void _start_two_body_svg_path( std::ostream &out, const std::function(uint64_t tick, uint32_t qubit)> &coords, uint64_t tick, - SpanRef terms, - std::vector> &pts_workspace) { + SpanRef terms) { auto a = coords(tick, terms[0].qubit_value()); auto b = coords(tick, terms[1].qubit_value()); auto dif = b - a; @@ -774,7 +773,6 @@ void _start_one_body_svg_path( const std::function(uint64_t tick, uint32_t qubit)> &coords, uint64_t tick, SpanRef terms, - std::vector> &pts_workspace, size_t scale) { auto c = coords(tick, terms[0].qubit_value()); out << " 2) { _start_many_body_svg_path(out, coords, tick, terms, pts_workspace); } else if (terms.size() == 2) { - _start_two_body_svg_path(out, coords, tick, terms, pts_workspace); + _start_two_body_svg_path(out, coords, tick, terms); } else if (terms.size() == 1) { - _start_one_body_svg_path(out, coords, tick, terms, pts_workspace, scale); + _start_one_body_svg_path(out, coords, tick, terms, scale); } } -void DetectorSliceSet::write_svg_diagram_to(std::ostream &out) const { - size_t num_cols = (uint64_t)ceil(sqrt((double)num_ticks)); - size_t num_rows = num_ticks / num_cols; - while (num_cols * num_rows < num_ticks) { - num_rows++; - } - while (num_cols * num_rows >= num_ticks + num_rows) { - num_cols--; +void DetectorSliceSet::write_svg_diagram_to(std::ostream &out, size_t num_rows) const { + size_t num_cols; + if (num_rows == 0) { + num_cols = (uint64_t)ceil(sqrt((double)num_ticks)); + num_rows = num_ticks / num_cols; + while (num_cols * num_rows < num_ticks) { + num_rows++; + } + while (num_cols * num_rows >= num_ticks + num_rows) { + num_cols--; + } + } else { + num_cols = (num_ticks + num_rows - 1) / num_rows; } auto coordsys = FlattenedCoords::from(*this, 32); diff --git a/src/stim/diagram/detector_slice/detector_slice_set.h b/src/stim/diagram/detector_slice/detector_slice_set.h index 76314eba..1fdeffa9 100644 --- a/src/stim/diagram/detector_slice/detector_slice_set.h +++ b/src/stim/diagram/detector_slice/detector_slice_set.h @@ -65,7 +65,7 @@ struct DetectorSliceSet { std::string str() const; void write_text_diagram_to(std::ostream &out) const; - void write_svg_diagram_to(std::ostream &out) const; + void write_svg_diagram_to(std::ostream &out, size_t num_rows = 0) const; void write_svg_contents_to( std::ostream &out, const std::function(uint32_t qubit)> &unscaled_coords, diff --git a/src/stim/diagram/timeline/timeline_svg_drawer.cc b/src/stim/diagram/timeline/timeline_svg_drawer.cc index 59a3ab02..67621fa4 100644 --- a/src/stim/diagram/timeline/timeline_svg_drawer.cc +++ b/src/stim/diagram/timeline/timeline_svg_drawer.cc @@ -808,7 +808,8 @@ void DiagramTimelineSvgDrawer::make_diagram_write_to( uint64_t tick_slice_start, uint64_t tick_slice_num, DiagramTimelineSvgDrawerMode mode, - SpanRef filter) { + SpanRef filter, + size_t num_rows) { uint64_t circuit_num_ticks = circuit.count_ticks(); auto circuit_has_ticks = circuit_num_ticks > 0; auto num_qubits = circuit.count_qubits(); @@ -825,8 +826,13 @@ void DiagramTimelineSvgDrawer::make_diagram_write_to( obj.coord_sys = FlattenedCoords::from(obj.detector_slice_set, GATE_PITCH); obj.coord_sys.size.xyz[0] += TIME_SLICE_PADDING * 2; obj.coord_sys.size.xyz[1] += TIME_SLICE_PADDING * 2; - obj.num_cols = (uint64_t)ceil(sqrt((double)tick_slice_num)); - obj.num_rows = tick_slice_num / obj.num_cols; + if (num_rows == 0) { + obj.num_cols = (uint64_t)ceil(sqrt((double)tick_slice_num)); + obj.num_rows = tick_slice_num / obj.num_cols; + } else { + obj.num_rows = num_rows; + obj.num_cols = (tick_slice_num + num_rows - 1) / num_rows; + } while (obj.num_cols * obj.num_rows < tick_slice_num) { obj.num_rows++; } @@ -855,9 +861,9 @@ void DiagramTimelineSvgDrawer::make_diagram_write_to( auto w = obj.m2x(obj.cur_moment) - GATE_PITCH * 0.5f; svg_out << R"SVG( 1) { auto k = 0; svg_out << "\n"; - for (uint64_t col = 0; col < obj.num_cols; col++) { - for (uint64_t row = 0; row < obj.num_rows && row * obj.num_cols + col < tick_slice_num; row++) { + for (uint64_t row = 0; row < obj.num_rows; row++) { + for (uint64_t col = 0; col < obj.num_cols && row * obj.num_cols + col < tick_slice_num; col++) { auto sw = obj.coord_sys.size.xyz[0]; auto sh = obj.coord_sys.size.xyz[1]; std::stringstream id_ss; + auto tick = k + tick_slice_start; // the absolute tick id_ss << "tick_border:" << k; id_ss << ":" << row << "_" << col; - id_ss << ":" << k + tick_slice_start; // the absolute tick + id_ss << ":" << tick; + + svg_out << ""; + svg_out << "Tick " << tick; + svg_out << "\n"; svg_out << "\n"; + + k++; } } svg_out << "\n"; diff --git a/src/stim/diagram/timeline/timeline_svg_drawer.h b/src/stim/diagram/timeline/timeline_svg_drawer.h index 5d0bef1c..cbc35bbe 100644 --- a/src/stim/diagram/timeline/timeline_svg_drawer.h +++ b/src/stim/diagram/timeline/timeline_svg_drawer.h @@ -64,7 +64,8 @@ struct DiagramTimelineSvgDrawer { uint64_t tick_slice_start, uint64_t tick_slice_num, DiagramTimelineSvgDrawerMode mode, - stim::SpanRef det_coord_filter); + stim::SpanRef det_coord_filter, + size_t num_rows = 0); void do_start_repeat(const CircuitTimelineLoopData &loop_data); void do_end_repeat(const CircuitTimelineLoopData &loop_data); diff --git a/src/stim/mem/bitword_128_sse.h b/src/stim/mem/bitword_128_sse.h index 05aafde8..a84e9875 100644 --- a/src/stim/mem/bitword_128_sse.h +++ b/src/stim/mem/bitword_128_sse.h @@ -53,6 +53,8 @@ struct bitword<128> { } inline bitword<128>(__m128i val) : val(val) { } + inline bitword<128>(std::array val) : val{_mm_set_epi64x(val[1], val[0])} { + } inline bitword<128>(uint64_t val) : val{_mm_set_epi64x(0, val)} { } inline bitword<128>(int64_t val) : val{_mm_set_epi64x(-(val < 0), val)} { diff --git a/src/stim/mem/bitword_256_avx.h b/src/stim/mem/bitword_256_avx.h index 6c35833c..4b5959f9 100644 --- a/src/stim/mem/bitword_256_avx.h +++ b/src/stim/mem/bitword_256_avx.h @@ -52,6 +52,8 @@ struct bitword<256> { } inline bitword<256>(__m256i val) : val(val) { } + inline bitword<256>(std::array val) : val{_mm256_set_epi64x(val[3], val[2], val[1], val[0])} { + } inline bitword<256>(uint64_t val) : val{_mm256_set_epi64x(0, 0, 0, val)} { } inline bitword<256>(int64_t val) : val{_mm256_set_epi64x(-(val < 0), -(val < 0), -(val < 0), val)} { diff --git a/src/stim/mem/bitword_64.h b/src/stim/mem/bitword_64.h index 3f8d588a..4a4a16ec 100644 --- a/src/stim/mem/bitword_64.h +++ b/src/stim/mem/bitword_64.h @@ -47,6 +47,8 @@ struct bitword<64> { inline constexpr bitword<64>() : val{} { } + inline bitword<64>(std::array val) : val{val[0]} { + } inline constexpr bitword<64>(uint64_t v) : val{v} { } inline constexpr bitword<64>(int64_t v) : val{(uint64_t)v} { diff --git a/src/stim/mem/simd_word.test.cc b/src/stim/mem/simd_word.test.cc index 3ab23583..b0777b4c 100644 --- a/src/stim/mem/simd_word.test.cc +++ b/src/stim/mem/simd_word.test.cc @@ -149,3 +149,13 @@ TEST_EACH_WORD_SIZE_W(simd_word, ordering, { ASSERT_TRUE(!(simd_word(2) < simd_word(2))); ASSERT_TRUE(!(simd_word(3) < simd_word(2))); }) + +TEST_EACH_WORD_SIZE_W(simd_word, from_u64_array, { + std::array expected; + for (size_t k = 0; k < expected.size(); k++) { + expected[k] = k * 3 + 1; + } + simd_word w(expected); + std::array actual = w.to_u64_array(); + ASSERT_EQ(actual, expected); +}) diff --git a/src/stim/simulators/frame_simulator.pybind.cc b/src/stim/simulators/frame_simulator.pybind.cc index 05386a4c..ff1a072c 100644 --- a/src/stim/simulators/frame_simulator.pybind.cc +++ b/src/stim/simulators/frame_simulator.pybind.cc @@ -870,4 +870,118 @@ void stim_pybind::pybind_frame_simulator_methods( )DOC") .data()); + + c.def( + "copy", + [](const FrameSimulator &self, bool copy_rng, pybind11::object &seed) { + if (copy_rng && !seed.is_none()) { + throw std::invalid_argument("seed and copy_rng are incompatible"); + } + + FrameSimulator copy = self; + if (!copy_rng || !seed.is_none()) { + copy.rng = make_py_seeded_rng(seed); + } + return copy; + }, + pybind11::kw_only(), + pybind11::arg("copy_rng") = false, + pybind11::arg("seed") = pybind11::none(), + clean_doc_string(R"DOC( + @signature def copy(self, *, copy_rng: bool = False, seed: Optional[int] = None) -> stim.FlipSimulator: + Returns a simulator with the same internal state, except perhaps its prng. + + Args: + copy_rng: Defaults to False. When False, the copy's pseudo random number + generator is reinitialized with a random seed instead of being a copy + of the original simulator's pseudo random number generator. This + causes the copy and the original to sample independent randomness, + instead of identical randomness, for future random operations. When set + to true, the copy will have the exact same pseudo random number + generator state as the original, and so will produce identical results + if told to do the same noisy operations. This argument is incompatible + with the `seed` argument. + + seed: PARTIALLY determines simulation results by deterministically seeding + the random number generator. + + Must be None or an integer in range(2**64). + + Defaults to None. When None, the prng state is either copied from the + original simulator or reseeded from system entropy, depending on the + copy_rng argument. + + When set to an integer, making the exact same series calls on the exact + same machine with the exact same version of Stim will produce the exact + same simulation results. + + CAUTION: simulation results *WILL NOT* be consistent between versions of + Stim. This restriction is present to make it possible to have future + optimizations to the random sampling, and is enforced by introducing + intentional differences in the seeding strategy from version to version. + + CAUTION: simulation results *MAY NOT* be consistent across machines that + differ in the width of supported SIMD instructions. For example, using + the same seed on a machine that supports AVX instructions and one that + only supports SSE instructions may produce different simulation results. + + CAUTION: simulation results *MAY NOT* be consistent if you vary how the + circuit is executed. For example, reordering whether a reset on one + qubit happens before or after a reset on another qubit can result in + different measurement results being observed starting from the same + seed. + + Returns: + The copy of the simulator. + + Examples: + >>> import stim + >>> import numpy as np + + >>> s1 = stim.FlipSimulator(batch_size=256) + >>> s1.set_pauli_flip('X', qubit_index=2, instance_index=3) + >>> s2 = s1.copy() + >>> s2 is s1 + False + >>> s2.peek_pauli_flips() == s1.peek_pauli_flips() + True + + >>> s1 = stim.FlipSimulator(batch_size=256) + >>> s2 = s1.copy(copy_rng=True) + >>> s1.do(stim.Circuit("X_ERROR(0.25) 0 \n M 0")) + >>> s2.do(stim.Circuit("X_ERROR(0.25) 0 \n M 0")) + >>> np.array_equal(s1.get_measurement_flips(), s2.get_measurement_flips()) + True + )DOC") + .data()); + + c.def( + "reset", + [](FrameSimulator &self) { + self.reset_all(); + }, + clean_doc_string(R"DOC( + Resets the simulator's state, so it can be reused for another simulation. + + This empties the measurement flip history, empties the detector flip history, + and zeroes the observable flip state. It also resets all qubits to |0>. If + stabilizer randomization is disabled, this zeros all pauli flips data. Otherwise + it randomizes all pauli flips to be I or Z with equal probability. + + Examples: + >>> import stim + >>> sim = stim.FlipSimulator(batch_size=256) + >>> sim.do(stim.Circuit("M(0.1) 9")) + >>> sim.num_qubits + 10 + >>> sim.get_measurement_flips().shape + (1, 256) + + >>> sim.reset() + >>> sim.num_qubits + 10 + >>> sim.get_measurement_flips().shape + (0, 256) + )DOC") + .data()); } diff --git a/src/stim/stabilizers/pauli_string.pybind.cc b/src/stim/stabilizers/pauli_string.pybind.cc index 8fd808a3..bcc68ac7 100644 --- a/src/stim/stabilizers/pauli_string.pybind.cc +++ b/src/stim/stabilizers/pauli_string.pybind.cc @@ -826,6 +826,13 @@ void stim_pybind::pybind_pauli_string_methods(pybind11::module &m, pybind11::cla }, clean_doc_string(R"DOC( Returns the length the pauli string; the number of qubits it operates on. + + Examples: + >>> import stim + >>> len(stim.PauliString("XY_ZZ")) + 5 + >>> len(stim.PauliString("X0*Z99")) + 100 )DOC") .data()); diff --git a/src/stim/stabilizers/tableau.pybind.cc b/src/stim/stabilizers/tableau.pybind.cc index 7e9a3ba9..6d9849b6 100644 --- a/src/stim/stabilizers/tableau.pybind.cc +++ b/src/stim/stabilizers/tableau.pybind.cc @@ -838,7 +838,16 @@ void stim_pybind::pybind_tableau_methods(pybind11::module &m, pybind11::class_ &self) { return self.num_qubits; }, - "Returns the number of qubits operated on by the tableau."); + clean_doc_string(R"DOC( + Returns the number of qubits operated on by the tableau. + + Examples: + >>> import stim + >>> t = stim.Tableau.from_named_gate("CNOT") + >>> len(t) + 2 + )DOC") + .data()); c.def("__str__", &Tableau::str, "Returns a text description."); diff --git a/src/stim/util_top/circuit_vs_amplitudes.cc b/src/stim/util_top/circuit_vs_amplitudes.cc index 154eabe5..6a56219b 100644 --- a/src/stim/util_top/circuit_vs_amplitudes.cc +++ b/src/stim/util_top/circuit_vs_amplitudes.cc @@ -1,6 +1,6 @@ #include "stim/util_top/circuit_vs_amplitudes.h" -#include "circuit_inverse_unitary.h" +#include "stim/util_top/circuit_inverse_unitary.h" #include "stim/simulators/tableau_simulator.h" #include "stim/simulators/vector_simulator.h" #include "stim/util_bot/twiddle.h" diff --git a/testdata/anticommuting_detslice.svg b/testdata/anticommuting_detslice.svg index 76e9814f..e4502e15 100644 --- a/testdata/anticommuting_detslice.svg +++ b/testdata/anticommuting_detslice.svg @@ -42,9 +42,13 @@ M +Tick 0 - - - +Tick 1 + +Tick 2 + +Tick 3 + \ No newline at end of file diff --git a/testdata/bezier_time_slice.svg b/testdata/bezier_time_slice.svg index 1f132c86..47291d78 100644 --- a/testdata/bezier_time_slice.svg +++ b/testdata/bezier_time_slice.svg @@ -26,7 +26,9 @@ +Tick 0 - +Tick 1 + \ No newline at end of file diff --git a/testdata/circuit_all_ops_detslice.svg b/testdata/circuit_all_ops_detslice.svg index a917974c..6a654cdf 100644 --- a/testdata/circuit_all_ops_detslice.svg +++ b/testdata/circuit_all_ops_detslice.svg @@ -561,19 +561,33 @@ Zrec +Tick 0 - - - - - - - - - - - - - +Tick 1 + +Tick 2 + +Tick 3 + +Tick 4 + +Tick 5 + +Tick 6 + +Tick 7 + +Tick 8 + +Tick 9 + +Tick 10 + +Tick 11 + +Tick 12 + +Tick 13 + \ No newline at end of file diff --git a/testdata/circuit_all_ops_timeslice.svg b/testdata/circuit_all_ops_timeslice.svg index a917974c..6a654cdf 100644 --- a/testdata/circuit_all_ops_timeslice.svg +++ b/testdata/circuit_all_ops_timeslice.svg @@ -561,19 +561,33 @@ Zrec +Tick 0 - - - - - - - - - - - - - +Tick 1 + +Tick 2 + +Tick 3 + +Tick 4 + +Tick 5 + +Tick 6 + +Tick 7 + +Tick 8 + +Tick 9 + +Tick 10 + +Tick 11 + +Tick 12 + +Tick 13 + \ No newline at end of file diff --git a/testdata/circuit_diagram_timeline_svg_chained_loops.svg b/testdata/circuit_diagram_timeline_svg_chained_loops.svg index cd3bbbd0..3ef5e37f 100644 --- a/testdata/circuit_diagram_timeline_svg_chained_loops.svg +++ b/testdata/circuit_diagram_timeline_svg_chained_loops.svg @@ -46,16 +46,27 @@ Z +Tick 0 - - - - - - - - - - +Tick 1 + +Tick 2 + +Tick 3 + +Tick 4 + +Tick 5 + +Tick 6 + +Tick 7 + +Tick 8 + +Tick 9 + +Tick 10 + \ No newline at end of file diff --git a/testdata/detslice-with-ops_surface_code.svg b/testdata/detslice-with-ops_surface_code.svg index e79153bd..6463895f 100644 --- a/testdata/detslice-with-ops_surface_code.svg +++ b/testdata/detslice-with-ops_surface_code.svg @@ -1271,19 +1271,33 @@ H +Tick 0 - - - - - - - - - - - - - +Tick 1 + +Tick 2 + +Tick 3 + +Tick 4 + +Tick 5 + +Tick 6 + +Tick 7 + +Tick 8 + +Tick 9 + +Tick 10 + +Tick 11 + +Tick 12 + +Tick 13 + \ No newline at end of file diff --git a/testdata/observable_slices.svg b/testdata/observable_slices.svg index 6bff67cb..6d4aa7be 100644 --- a/testdata/observable_slices.svg +++ b/testdata/observable_slices.svg @@ -145,18 +145,31 @@ M +Tick 0 - - - - - - - - - - - - +Tick 1 + +Tick 2 + +Tick 3 + +Tick 4 + +Tick 5 + +Tick 6 + +Tick 7 + +Tick 8 + +Tick 9 + +Tick 10 + +Tick 11 + +Tick 12 + \ No newline at end of file diff --git a/testdata/surface_code_full_time_detector_slice.svg b/testdata/surface_code_full_time_detector_slice.svg index ace2226d..7e7fdae6 100644 --- a/testdata/surface_code_full_time_detector_slice.svg +++ b/testdata/surface_code_full_time_detector_slice.svg @@ -2823,41 +2823,77 @@ M +Tick 0 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +Tick 1 + +Tick 2 + +Tick 3 + +Tick 4 + +Tick 5 + +Tick 6 + +Tick 7 + +Tick 8 + +Tick 9 + +Tick 10 + +Tick 11 + +Tick 12 + +Tick 13 + +Tick 14 + +Tick 15 + +Tick 16 + +Tick 17 + +Tick 18 + +Tick 19 + +Tick 20 + +Tick 21 + +Tick 22 + +Tick 23 + +Tick 24 + +Tick 25 + +Tick 26 + +Tick 27 + +Tick 28 + +Tick 29 + +Tick 30 + +Tick 31 + +Tick 32 + +Tick 33 + +Tick 34 + +Tick 35 + \ No newline at end of file diff --git a/testdata/surface_code_time_detector_slice.svg b/testdata/surface_code_time_detector_slice.svg index 01fbf938..3bf551e1 100644 --- a/testdata/surface_code_time_detector_slice.svg +++ b/testdata/surface_code_time_detector_slice.svg @@ -749,16 +749,27 @@ H +Tick 5 - - - - - - - - - - +Tick 6 + +Tick 7 + +Tick 8 + +Tick 9 + +Tick 10 + +Tick 11 + +Tick 12 + +Tick 13 + +Tick 14 + +Tick 15 + \ No newline at end of file diff --git a/testdata/surface_code_time_slice.svg b/testdata/surface_code_time_slice.svg index 38ede396..1606fe7f 100644 --- a/testdata/surface_code_time_slice.svg +++ b/testdata/surface_code_time_slice.svg @@ -373,16 +373,27 @@ H +Tick 5 - - - - - - - - - - +Tick 6 + +Tick 7 + +Tick 8 + +Tick 9 + +Tick 10 + +Tick 11 + +Tick 12 + +Tick 13 + +Tick 14 + +Tick 15 + \ No newline at end of file