mirror of
https://github.com/nqrduck/quackseq.git
synced 2024-11-21 13:32:25 +00:00
Phase cycling.
This commit is contained in:
parent
bdb5dd7cd5
commit
fc99ded526
6 changed files with 176 additions and 96 deletions
|
@ -1,6 +1,7 @@
|
||||||
"""Options for the pulse parameters. Options can be of different types, for example boolean, numeric or function. Generally pulse parameters have different values for the different events in a pulse sequence."""
|
"""Options for the pulse parameters. Options can be of different types, for example boolean, numeric or function. Generally pulse parameters have different values for the different events in a pulse sequence."""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
from collections import OrderedDict
|
||||||
from quackseq.functions import Function
|
from quackseq.functions import Function
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -82,7 +83,6 @@ class BooleanOption(Option):
|
||||||
def set_value(self, value):
|
def set_value(self, value):
|
||||||
"""Sets the value of the option."""
|
"""Sets the value of the option."""
|
||||||
self.value = value
|
self.value = value
|
||||||
self.value_changed.emit()
|
|
||||||
|
|
||||||
|
|
||||||
class NumericOption(Option):
|
class NumericOption(Option):
|
||||||
|
@ -105,7 +105,7 @@ class NumericOption(Option):
|
||||||
is_float (bool): If the value is a float.
|
is_float (bool): If the value is a float.
|
||||||
min_value: The minimum value of the option.
|
min_value: The minimum value of the option.
|
||||||
max_value: The maximum value of the option.
|
max_value: The maximum value of the option.
|
||||||
slider (bool): If the option should be displayed as a slider. This is not used for the pulseq module, but visualizations can use this information.
|
slider (bool): If the option should be displayed as a slider. This is not used for the quackseq module, but visualizations can use this information.
|
||||||
"""
|
"""
|
||||||
super().__init__(name, value)
|
super().__init__(name, value)
|
||||||
self.is_float = is_float
|
self.is_float = is_float
|
||||||
|
@ -115,12 +115,12 @@ class NumericOption(Option):
|
||||||
|
|
||||||
def set_value(self, value):
|
def set_value(self, value):
|
||||||
"""Sets the value of the option."""
|
"""Sets the value of the option."""
|
||||||
if value < self.min_value:
|
if self.min_value is None or self.max_value is None:
|
||||||
self.value = self.min_value
|
self.value = value
|
||||||
self.value_changed.emit()
|
return
|
||||||
elif value >= self.max_value:
|
|
||||||
self.value = self.max_value
|
if self.min_value <= value <= self.max_value:
|
||||||
self.value_changed.emit()
|
self.value = value
|
||||||
else:
|
else:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Value {value} is not in the range of {self.min_value} to {self.max_value}. This should have been caught earlier."
|
f"Value {value} is not in the range of {self.min_value} to {self.max_value}. This should have been caught earlier."
|
||||||
|
@ -237,31 +237,50 @@ class FunctionOption(Option):
|
||||||
class TableOption(Option):
|
class TableOption(Option):
|
||||||
"""A table option has rows and columns and can be used to store a table of values.
|
"""A table option has rows and columns and can be used to store a table of values.
|
||||||
|
|
||||||
The table option acts as a 'meta' option, which means that we can add different types of options to the table as rows.
|
The table option acts as a 'meta' option, which means that we can add different types of options to the table as columns.
|
||||||
Associated with every row we can add a number of different values.
|
Associated with every row we can add a number of different values.
|
||||||
The number of rows can be adjusted at runtime.
|
The number of rows can be adjusted at runtime.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, name: str, value = None) -> None:
|
def __init__(self, name: str, value=None) -> None:
|
||||||
"""Initializes the table option."""
|
"""Initializes the table option."""
|
||||||
super().__init__(name, value)
|
super().__init__(name, value)
|
||||||
|
|
||||||
self.options = []
|
self.columns = []
|
||||||
self.n_rows = 0
|
self.n_rows = 0
|
||||||
|
|
||||||
def set_value(self, value):
|
def add_column(self, column_name: str, option: Option, default_value) -> None:
|
||||||
"""Sets the value of the option."""
|
"""Adds an option to the table as column.
|
||||||
self.value = value
|
|
||||||
|
|
||||||
def add_option(self, option: Option) -> None:
|
Options are added as columns.
|
||||||
"""Adds an option to the table.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
option (Option): The option to add.
|
option (Option): The class of the option to add.
|
||||||
|
column_name (str): The name of the column.
|
||||||
|
default_value: The default value of the column.
|
||||||
"""
|
"""
|
||||||
self.options.append(option)
|
column = self.Column(column_name, option, default_value, self.n_rows)
|
||||||
|
# Add the column to the table
|
||||||
|
self.columns.append(column)
|
||||||
|
|
||||||
def set_n_rows(self, n_rows : int) -> None:
|
def set_value(self, values: list) -> None:
|
||||||
|
"""Sets the value of the option.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
values: The values of the different options in the table.
|
||||||
|
"""
|
||||||
|
for i, column in enumerate(values):
|
||||||
|
self.columns[i].set_row_values(column)
|
||||||
|
|
||||||
|
def get_value(self) -> list:
|
||||||
|
"""Gets the value of the option.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: The values of the different options in the table.
|
||||||
|
"""
|
||||||
|
return [column.get_values() for column in self.columns]
|
||||||
|
|
||||||
|
def set_n_rows(self, n_rows: int) -> None:
|
||||||
"""Sets the number of rows in the table.
|
"""Sets the number of rows in the table.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
@ -269,6 +288,24 @@ class TableOption(Option):
|
||||||
"""
|
"""
|
||||||
self.n_rows = n_rows
|
self.n_rows = n_rows
|
||||||
|
|
||||||
|
# Now we need to set the number of rows for all the options in the table, the last value is repeated if the number of rows is increased
|
||||||
|
for column in self.columns:
|
||||||
|
column.update_n_rows(n_rows)
|
||||||
|
|
||||||
|
def get_option_by_name(self, name: str) -> Option:
|
||||||
|
"""Gets an option by its name.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): The name of the option.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Option: The option with the given name.
|
||||||
|
"""
|
||||||
|
for option in self.options:
|
||||||
|
if option.name == name:
|
||||||
|
return option
|
||||||
|
raise ValueError(f"Option with name {name} not found")
|
||||||
|
|
||||||
def to_json(self):
|
def to_json(self):
|
||||||
"""Returns a json representation of the option.
|
"""Returns a json representation of the option.
|
||||||
|
|
||||||
|
@ -294,4 +331,51 @@ class TableOption(Option):
|
||||||
obj = cls(data["name"], data["value"])
|
obj = cls(data["name"], data["value"])
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
class Column:
|
||||||
|
"""Defines a column option for a table option.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): The name of the option.
|
||||||
|
type (type): The type of the option.
|
||||||
|
default_value: The default value of the option.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, name: str, type, default_value, n_rows: int) -> None:
|
||||||
|
"""Initializes the column option."""
|
||||||
|
self.name = name
|
||||||
|
self.type = type
|
||||||
|
self.default_value = default_value
|
||||||
|
|
||||||
|
self.options = []
|
||||||
|
|
||||||
|
def update_n_rows(self, n_rows: int) -> None:
|
||||||
|
"""Updates the number of rows in the column.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
n_rows (int): The number of rows.
|
||||||
|
"""
|
||||||
|
if len(self.options) < n_rows:
|
||||||
|
self.options.extend(
|
||||||
|
self.type(self.name, self.default_value)
|
||||||
|
for i in range(n_rows - len(self.options))
|
||||||
|
)
|
||||||
|
elif len(self.options) > n_rows:
|
||||||
|
self.options = self.options[:n_rows]
|
||||||
|
|
||||||
|
def set_row_values(self, values: list) -> None:
|
||||||
|
"""Sets the values of the options in the column.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
values: The values of the options in the column.
|
||||||
|
"""
|
||||||
|
for i, value in enumerate(values):
|
||||||
|
self.options[i].set_value(value)
|
||||||
|
|
||||||
|
def get_values(self) -> list:
|
||||||
|
"""Gets the values of the options in the column.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: The values of the options in the column.
|
||||||
|
"""
|
||||||
|
return [option.value for option in self.options]
|
||||||
|
|
|
@ -15,7 +15,8 @@ class PhaseTable:
|
||||||
def __init__(self, quackseq):
|
def __init__(self, quackseq):
|
||||||
"""Initializes the phase table."""
|
"""Initializes the phase table."""
|
||||||
self.quackseq = quackseq
|
self.quackseq = quackseq
|
||||||
self.readout_scheme = ReadoutScheme(self)
|
# Set phase array to default value
|
||||||
|
self.phase_array = np.array([])
|
||||||
self.generate_phase_array()
|
self.generate_phase_array()
|
||||||
|
|
||||||
def generate_phase_array(self):
|
def generate_phase_array(self):
|
||||||
|
@ -186,9 +187,6 @@ class PhaseTable:
|
||||||
# First set the phase array
|
# First set the phase array
|
||||||
self.phase_array = phase_array
|
self.phase_array = phase_array
|
||||||
|
|
||||||
# Then update the readout scheme (always reset it)
|
|
||||||
self.readout_scheme.update_readout_scheme()
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def phase_array(self) -> np.array:
|
def phase_array(self) -> np.array:
|
||||||
"""The phase array of the sequence."""
|
"""The phase array of the sequence."""
|
||||||
|
@ -201,64 +199,11 @@ class PhaseTable:
|
||||||
@property
|
@property
|
||||||
def n_phase_cycles(self) -> int:
|
def n_phase_cycles(self) -> int:
|
||||||
"""The number of phase cycles in the sequence."""
|
"""The number of phase cycles in the sequence."""
|
||||||
|
# Calculate the number of phase cycles
|
||||||
|
self.generate_phase_array()
|
||||||
return self.phase_array.shape[0]
|
return self.phase_array.shape[0]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def n_parameters(self) -> int:
|
def n_parameters(self) -> int:
|
||||||
"""The number of TX pulse parameters in the sequence."""
|
"""The number of TX pulse parameters in the sequence."""
|
||||||
return self.phase_array.shape[1]
|
return self.phase_array.shape[1]
|
||||||
|
|
||||||
|
|
||||||
class ReadoutScheme:
|
|
||||||
"""Readout Scheme for the phase table.
|
|
||||||
|
|
||||||
The rows are the phase cycles of the sequence.
|
|
||||||
|
|
||||||
The columns have two different types of options:
|
|
||||||
- The phase value of the phase cycle.
|
|
||||||
- The function that is applied to the phase cycle. Usually this is just +1, -1 or 0.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, phase_table: PhaseTable) -> None:
|
|
||||||
"""Initializes the ReadoutOption."""
|
|
||||||
self.phase_table = phase_table
|
|
||||||
|
|
||||||
def update_readout_scheme(self):
|
|
||||||
"""Update the readout scheme of the sequence. Whenever the phase array is updated it will be reset."""
|
|
||||||
|
|
||||||
self.readout_scheme = np.zeros(
|
|
||||||
(self.phase_table.n_phase_cycles, self.phase_table.n_parameters)
|
|
||||||
)
|
|
||||||
|
|
||||||
def set_phase_cycle(
|
|
||||||
self, phase_cycle: int, phase_shift: float, function: str
|
|
||||||
) -> None:
|
|
||||||
"""Sets the phase shift and function of a phase cycle.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
phase_cycle (int): The phase cycle.
|
|
||||||
phase_shift (float): The phase shift.
|
|
||||||
function (str): The function.
|
|
||||||
"""
|
|
||||||
self.readout_scheme[phase_cycle] = [phase_shift, function]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def readout_scheme(self) -> np.array:
|
|
||||||
"""The readout scheme of the sequence."""
|
|
||||||
return self._readout_scheme
|
|
||||||
|
|
||||||
@readout_scheme.setter
|
|
||||||
def readout_scheme(self, readout_scheme: list) -> None:
|
|
||||||
"""Sets the readout scheme of the sequence.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
readout_scheme (list): The readout scheme.
|
|
||||||
"""
|
|
||||||
# Sanity check
|
|
||||||
if len(readout_scheme) != self.phase_table.n_phase_cycles:
|
|
||||||
raise ValueError(
|
|
||||||
f"Length of readout scheme ({len(readout_scheme)}) does not match the number of phase cycles ({self.phase_table.n_phase_cycles})"
|
|
||||||
)
|
|
||||||
|
|
||||||
self._readout_scheme = readout_scheme
|
|
|
@ -6,11 +6,13 @@ Todo:
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import logging
|
import logging
|
||||||
|
from typing import override
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from numpy.core.multiarray import array as array
|
from numpy.core.multiarray import array as array
|
||||||
|
|
||||||
from quackseq.options import (
|
from quackseq.options import (
|
||||||
|
TableOption,
|
||||||
BooleanOption,
|
BooleanOption,
|
||||||
FunctionOption,
|
FunctionOption,
|
||||||
NumericOption,
|
NumericOption,
|
||||||
|
@ -81,6 +83,16 @@ class PulseParameter:
|
||||||
return option
|
return option
|
||||||
raise ValueError(f"Option with name {name} not found")
|
raise ValueError(f"Option with name {name} not found")
|
||||||
|
|
||||||
|
def update_option(self, sequence: "QuackSequence") -> None:
|
||||||
|
"""Generic update option method for pulse parameters.
|
||||||
|
|
||||||
|
This can be implemented by subclasses to update the options of the pulse parameter whenever the parameter is called (e.g. in the GUI).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sequence (QuackSequence): The sequence to update the options from.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class TXPulse(PulseParameter):
|
class TXPulse(PulseParameter):
|
||||||
"""Basic TX Pulse Parameter. It includes options for the relative amplitude, the phase and the pulse shape.
|
"""Basic TX Pulse Parameter. It includes options for the relative amplitude, the phase and the pulse shape.
|
||||||
|
@ -168,6 +180,8 @@ class RXReadout(PulseParameter):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
RX = "Enable RX Readout"
|
RX = "Enable RX Readout"
|
||||||
|
READOUT_SCHEME = "Readout Scheme"
|
||||||
|
PHASE = "Phase"
|
||||||
|
|
||||||
def __init__(self, name) -> None:
|
def __init__(self, name) -> None:
|
||||||
"""Initializes the RX Readout PulseParameter.
|
"""Initializes the RX Readout PulseParameter.
|
||||||
|
@ -177,6 +191,27 @@ class RXReadout(PulseParameter):
|
||||||
super().__init__(name)
|
super().__init__(name)
|
||||||
self.add_option(BooleanOption(self.RX, False))
|
self.add_option(BooleanOption(self.RX, False))
|
||||||
|
|
||||||
|
# Readout Scheme:
|
||||||
|
readout_option = TableOption(self.READOUT_SCHEME)
|
||||||
|
|
||||||
|
# Add Phase Option to Readout Scheme
|
||||||
|
phase_option = NumericOption
|
||||||
|
|
||||||
|
readout_option.add_column(self.PHASE, phase_option, 0)
|
||||||
|
|
||||||
|
# Set number of rows to default value
|
||||||
|
readout_option.set_n_rows(1)
|
||||||
|
|
||||||
|
self.add_option(readout_option)
|
||||||
|
|
||||||
|
@override
|
||||||
|
def update_option(self, sequence: "QuackSequence") -> None:
|
||||||
|
"""Adjusts the number of rows in the table option based on the number of phase cycles in the sequence."""
|
||||||
|
n_phase_cycles = sequence.get_n_phase_cycles()
|
||||||
|
readout_option = self.get_option_by_name(self.READOUT_SCHEME)
|
||||||
|
readout_option.set_n_rows(n_phase_cycles)
|
||||||
|
logger.debug(f"Updated RX Readout option with {n_phase_cycles} rows")
|
||||||
|
|
||||||
|
|
||||||
class Gate(PulseParameter):
|
class Gate(PulseParameter):
|
||||||
"""Basic PulseParameter for the Gate. It includes an option for the Gate state.
|
"""Basic PulseParameter for the Gate. It includes an option for the Gate state.
|
||||||
|
|
|
@ -242,6 +242,12 @@ class QuackSequence(PulseSequence):
|
||||||
|
|
||||||
self.phase_table = PhaseTable(self)
|
self.phase_table = PhaseTable(self)
|
||||||
|
|
||||||
|
def update_options(self) -> None:
|
||||||
|
"""Updates the options of the pulse parameters."""
|
||||||
|
for event in self.events:
|
||||||
|
for pulse_parameter in event.parameters.values():
|
||||||
|
pulse_parameter.update_option(self)
|
||||||
|
|
||||||
def add_blank_event(self, event_name: str, duration: float) -> Event:
|
def add_blank_event(self, event_name: str, duration: float) -> Event:
|
||||||
"""Adds a blank event to the pulse sequence.
|
"""Adds a blank event to the pulse sequence.
|
||||||
|
|
||||||
|
@ -280,6 +286,8 @@ class QuackSequence(PulseSequence):
|
||||||
self.set_tx_phase(event, phase)
|
self.set_tx_phase(event, phase)
|
||||||
self.set_tx_shape(event, shape)
|
self.set_tx_shape(event, shape)
|
||||||
|
|
||||||
|
self.update_options()
|
||||||
|
|
||||||
return event
|
return event
|
||||||
|
|
||||||
def add_readout_event(self, event_name: str, duration: float) -> Event:
|
def add_readout_event(self, event_name: str, duration: float) -> Event:
|
||||||
|
@ -360,6 +368,8 @@ class QuackSequence(PulseSequence):
|
||||||
TXPulse.N_PHASE_CYCLES
|
TXPulse.N_PHASE_CYCLES
|
||||||
).value = n_phase_cycles
|
).value = n_phase_cycles
|
||||||
|
|
||||||
|
self.update_options()
|
||||||
|
|
||||||
def set_tx_phase_cycle_group(
|
def set_tx_phase_cycle_group(
|
||||||
self, event: Event | str, phase_cycle_group: int
|
self, event: Event | str, phase_cycle_group: int
|
||||||
) -> None:
|
) -> None:
|
||||||
|
@ -376,6 +386,8 @@ class QuackSequence(PulseSequence):
|
||||||
TXPulse.PHASE_CYCLE_GROUP
|
TXPulse.PHASE_CYCLE_GROUP
|
||||||
).value = phase_cycle_group
|
).value = phase_cycle_group
|
||||||
|
|
||||||
|
self.update_options()
|
||||||
|
|
||||||
# RX Specific functions
|
# RX Specific functions
|
||||||
|
|
||||||
def set_rx(self, event: Event | str, rx: bool) -> None:
|
def set_rx(self, event: Event | str, rx: bool) -> None:
|
||||||
|
@ -390,30 +402,34 @@ class QuackSequence(PulseSequence):
|
||||||
|
|
||||||
event.parameters[self.RX_READOUT].get_option_by_name(RXReadout.RX).value = rx
|
event.parameters[self.RX_READOUT].get_option_by_name(RXReadout.RX).value = rx
|
||||||
|
|
||||||
def set_rx_readout_scheme(self, event: Event | str, readout_scheme: list) -> None:
|
def set_rx_phase(self, event: Event | str, phase: list) -> None:
|
||||||
"""Sets the readout scheme of the receiver.
|
"""Sets the phase of the receiver.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
event (Event | str): The event to set the readout scheme for or the name of the event
|
event (Event | str): The event to set the phase for or the name of the event
|
||||||
readout_scheme (list): The readout scheme of the receiver
|
phase (list): The phase of the receiver
|
||||||
"""
|
"""
|
||||||
if isinstance(event, str):
|
if isinstance(event, str):
|
||||||
event = self.get_event_by_name(event)
|
event = self.get_event_by_name(event)
|
||||||
|
|
||||||
# Check that the readout scheme is valid
|
rx_table = event.parameters[self.RX_READOUT].get_option_by_name(RXReadout.READOUT_SCHEME)
|
||||||
self.phase_table.generate_phase_array()
|
|
||||||
n_cycles = self.phase_table.n_phase_cycles
|
|
||||||
|
|
||||||
rows = len(readout_scheme)
|
# Get the actual option
|
||||||
|
phase_option = rx_table.get_option_by_name(RXReadout.PHASE)
|
||||||
|
|
||||||
if rows != n_cycles:
|
# Check that the number of phases is the same as the number of phase cycles
|
||||||
|
if len(phase) != self.get_n_phase_cycles():
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Readout scheme needs to have {n_cycles} cycles, but has {rows}"
|
f"Number of phases ({len(phase)}) needs to be the same as the number of phase cycles ({self.get_n_phase_cycles()})"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Old way - implement the sequence wide readout scheme here
|
# Set the values
|
||||||
#event.parameters[self.RX_READOUT].get_option_by_name(
|
phase_option.values = phase
|
||||||
# RXReadout.READOUT_SCHEME
|
|
||||||
#).value = readout_scheme
|
|
||||||
|
|
||||||
self.phase_table.readout_scheme.readout_scheme = readout_scheme
|
def get_n_phase_cycles(self) -> int:
|
||||||
|
"""Returns the number of phase cycles of the pulse sequence.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: The number of phase cycles
|
||||||
|
"""
|
||||||
|
return self.phase_table.n_phase_cycles
|
|
@ -26,6 +26,6 @@ def create_COMPFID():
|
||||||
# No phase shifiting of the receive data but weighting of -1 for the 45 degree pulse, +1 for the 135 degree pulse, -1 for the 225 degree pulse and +1 for the 315 degree pulse
|
# No phase shifiting of the receive data but weighting of -1 for the 45 degree pulse, +1 for the 135 degree pulse, -1 for the 225 degree pulse and +1 for the 315 degree pulse
|
||||||
readout_scheme = [0, 180, 0, 180]
|
readout_scheme = [0, 180, 0, 180]
|
||||||
|
|
||||||
COMPFID.set_rx_readout_scheme("rx", readout_scheme)
|
COMPFID.set_rx_phase("rx", readout_scheme)
|
||||||
|
|
||||||
return COMPFID
|
return COMPFID
|
||||||
|
|
|
@ -29,6 +29,6 @@ def create_SEPC() -> QuackSequence:
|
||||||
# Readout scheme for phase cycling TX pulses have the scheme 0 90 180 270
|
# Readout scheme for phase cycling TX pulses have the scheme 0 90 180 270
|
||||||
readout_scheme = [0, 90, 180, 270]
|
readout_scheme = [0, 90, 180, 270]
|
||||||
|
|
||||||
sepc.set_rx_readout_scheme("rx", readout_scheme)
|
sepc.set_rx_phase("rx", readout_scheme)
|
||||||
|
|
||||||
return sepc
|
return sepc
|
||||||
|
|
Loading…
Reference in a new issue