diff --git a/src/braket/analog_hamiltonian_simulator/rydberg/validators/device_capabilities_constants.py b/src/braket/analog_hamiltonian_simulator/rydberg/validators/device_capabilities_constants.py new file mode 100644 index 00000000..eb342b2b --- /dev/null +++ b/src/braket/analog_hamiltonian_simulator/rydberg/validators/device_capabilities_constants.py @@ -0,0 +1,36 @@ +from decimal import Decimal +from typing import Optional + +from braket.analog_hamiltonian_simulator.rydberg.validators.capabilities_constants import ( + CapabilitiesConstants, +) + + +class DeviceCapabilitiesConstants(CapabilitiesConstants): + MAX_SITES: int + SITE_PRECISION: Decimal + MAX_FILLED_SITES: int + MIN_ROW_DISTANCE: Decimal + + GLOBAL_TIME_PRECISION: Decimal + GLOBAL_AMPLITUDE_VALUE_PRECISION: Decimal + GLOBAL_AMPLITUDE_SLOPE_MAX: Decimal + GLOBAL_MIN_TIME_SEPARATION: Decimal + GLOBAL_DETUNING_VALUE_PRECISION: Decimal + GLOBAL_DETUNING_SLOPE_MAX: Decimal + GLOBAL_PHASE_VALUE_MIN: Decimal + GLOBAL_PHASE_VALUE_MAX: Decimal + GLOBAL_PHASE_VALUE_PRECISION: Decimal + + LOCAL_RYDBERG_CAPABILITIES: bool = False + LOCAL_MAGNITUDE_SLOPE_MAX: Optional[Decimal] + LOCAL_MIN_DISTANCE_BETWEEN_SHIFTED_SITES: Optional[Decimal] + LOCAL_TIME_PRECISION: Optional[Decimal] + LOCAL_MIN_TIME_SEPARATION: Optional[Decimal] + LOCAL_MAGNITUDE_SEQUENCE_VALUE_MIN: Optional[Decimal] + LOCAL_MAGNITUDE_SEQUENCE_VALUE_MAX: Optional[Decimal] + LOCAL_MAX_NONZERO_PATTERN_VALUES: Optional[Decimal] + + MAGNITUDE_PATTERN_VALUE_MIN: Optional[Decimal] + MAGNITUDE_PATTERN_VALUE_MAX: Optional[Decimal] + MAX_NET_DETUNING: Optional[Decimal] diff --git a/src/braket/analog_hamiltonian_simulator/rydberg/validators/device_ir_validator.py b/src/braket/analog_hamiltonian_simulator/rydberg/validators/device_ir_validator.py new file mode 100644 index 00000000..a080f6d2 --- /dev/null +++ b/src/braket/analog_hamiltonian_simulator/rydberg/validators/device_ir_validator.py @@ -0,0 +1,68 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. + +from braket.ir.ahs.program_v1 import Program + +from braket.analog_hamiltonian_simulator.rydberg.validators.device_capabilities_constants import ( + DeviceCapabilitiesConstants, +) +from braket.analog_hamiltonian_simulator.rydberg.validators.device_validators import ( + DeviceAtomArrangementValidator, + DeviceDrivingFieldValidator, + DeviceHamiltonianValidator, + DeviceLocalDetuningValidator, +) +from braket.analog_hamiltonian_simulator.rydberg.validators.physical_field import ( + PhysicalFieldValidator, +) +from braket.analog_hamiltonian_simulator.rydberg.validators.program import ProgramValidator +from braket.analog_hamiltonian_simulator.rydberg.validators.times_series import TimeSeriesValidator + + +def validate_program(program: Program, device_capabilities: DeviceCapabilitiesConstants) -> None: + """ + Validate the analog Hamiltonian simulation program has only one driving and shifting field, + and all the sequences have the same last time point. + + Args: + program (Program): An analog Hamiltonian simulation program + device_capabilities (CapabilitiesConstants): The capability constants for the simulator + """ + + ProgramValidator(capabilities=device_capabilities, **program.dict()) + DeviceAtomArrangementValidator( + capabilities=device_capabilities, **program.setup.ahs_register.dict() + ) + DeviceHamiltonianValidator( + LOCAL_RYDBERG_CAPABILITIES=device_capabilities.LOCAL_RYDBERG_CAPABILITIES, + **program.hamiltonian.dict() + ) + for d_fields in program.hamiltonian.drivingFields: + DeviceDrivingFieldValidator(capabilities=device_capabilities, **d_fields.dict()) + amplitude = d_fields.amplitude + phase = d_fields.phase + detuning = d_fields.detuning + + PhysicalFieldValidator(**amplitude.dict()) + TimeSeriesValidator(capabilities=device_capabilities, **amplitude.time_series.dict()) + + PhysicalFieldValidator(**phase.dict()) + TimeSeriesValidator(capabilities=device_capabilities, **phase.time_series.dict()) + + PhysicalFieldValidator(**detuning.dict()) + TimeSeriesValidator(capabilities=device_capabilities, **detuning.time_series.dict()) + for s_fields in program.hamiltonian.localDetuning: + DeviceLocalDetuningValidator(capabilities=device_capabilities, **s_fields.dict()) + magnitude = s_fields.magnitude + PhysicalFieldValidator(**magnitude.dict()) + TimeSeriesValidator(capabilities=device_capabilities, **magnitude.time_series.dict()) diff --git a/src/braket/analog_hamiltonian_simulator/rydberg/validators/device_validators/__init__.py b/src/braket/analog_hamiltonian_simulator/rydberg/validators/device_validators/__init__.py new file mode 100644 index 00000000..894e5e68 --- /dev/null +++ b/src/braket/analog_hamiltonian_simulator/rydberg/validators/device_validators/__init__.py @@ -0,0 +1,12 @@ +from braket.analog_hamiltonian_simulator.rydberg.validators.device_validators.device_atom_arrangement import ( # noqa: E501 F401 + DeviceAtomArrangementValidator, +) +from braket.analog_hamiltonian_simulator.rydberg.validators.device_validators.device_driving_field import ( # noqa: E501 F401 + DeviceDrivingFieldValidator, +) +from braket.analog_hamiltonian_simulator.rydberg.validators.device_validators.device_hamiltonian import ( # noqa: E501 F401 + DeviceHamiltonianValidator, +) +from braket.analog_hamiltonian_simulator.rydberg.validators.device_validators.device_local_detuning import ( # noqa: E501 F401 + DeviceLocalDetuningValidator, +) diff --git a/src/braket/analog_hamiltonian_simulator/rydberg/validators/device_validators/device_atom_arrangement.py b/src/braket/analog_hamiltonian_simulator/rydberg/validators/device_validators/device_atom_arrangement.py new file mode 100644 index 00000000..ca19d962 --- /dev/null +++ b/src/braket/analog_hamiltonian_simulator/rydberg/validators/device_validators/device_atom_arrangement.py @@ -0,0 +1,90 @@ +from decimal import Decimal +from typing import Tuple + +from pydantic.v1.class_validators import root_validator + +from braket.analog_hamiltonian_simulator.rydberg.validators.atom_arrangement import ( + AtomArrangementValidator, +) +from braket.analog_hamiltonian_simulator.rydberg.validators.device_capabilities_constants import ( + DeviceCapabilitiesConstants, +) + + +def _y_distance(site_1: Tuple[Decimal, Decimal], site_2: Tuple[Decimal, Decimal]) -> Decimal: + # Compute the y-separation between two sets of 2-D points, (x1, y1) and (x2, y2) + + return Decimal(abs(site_1[1] - site_2[1])) + + +class DeviceAtomArrangementValidator(AtomArrangementValidator): + capabilities: DeviceCapabilitiesConstants + + @root_validator(pre=True, skip_on_failure=True) + def sites_not_empty(cls, values): + sites = values["sites"] + if not sites: + raise ValueError("Sites can not be empty.") + return values + + # The maximum allowable precision in the coordinates is SITE_PRECISION + @root_validator(pre=True, skip_on_failure=True) + def sites_defined_with_right_precision(cls, values): + sites = values["sites"] + capabilities = values["capabilities"] + for idx, s in enumerate(sites): + if not all( + [Decimal(str(coordinate)) % capabilities.SITE_PRECISION == 0 for coordinate in s] + ): + raise ValueError( + f"Coordinates {idx}({s}) is defined with too high precision;" + f"they must be multiples of {capabilities.SITE_PRECISION} meters" + ) + return values + + # Number of sites must not exceeds MAX_SITES + @root_validator(pre=True, skip_on_failure=True) + def sites_not_too_many(cls, values): + sites = values["sites"] + capabilities = values["capabilities"] + num_sites = len(sites) + if num_sites > capabilities.MAX_SITES: + raise ValueError( + f"There are too many sites ({num_sites}); there must be at most " + f"{capabilities.MAX_SITES} sites" + ) + return values + + # The y coordinates of any two lattice sites must either be equal + # or differ by at least MIN_ROW_DISTANCE. + @root_validator(pre=True, skip_on_failure=True) + def sites_in_rows(cls, values): + sites = values["sites"] + capabilities = values["capabilities"] + sorted_sites = sorted(sites, key=lambda xy: xy[1]) + min_allowed_distance = capabilities.MIN_ROW_DISTANCE + if capabilities.LOCAL_RYDBERG_CAPABILITIES: + min_allowed_distance = Decimal("0.000002") + for s1, s2 in zip(sorted_sites[:-1], sorted_sites[1:]): + row_distance = _y_distance(s1, s2) + if row_distance == 0: + continue + if row_distance < min_allowed_distance: + raise ValueError( + f"Sites {s1} and site {s2} have y-separation ({row_distance}). It must " + f"either be exactly zero or not smaller than {min_allowed_distance} meters" + ) + return values + + # The number of filled lattice sites must not exceed MAX_FILLED_SITES. + @root_validator(pre=True, skip_on_failure=True) + def atom_number_limit(cls, values): + filling = values["filling"] + capabilities = values["capabilities"] + qubits = sum(filling) + if qubits > capabilities.MAX_FILLED_SITES: + raise ValueError( + f"Filling has {qubits} '1' entries; it must have not " + f"more than {capabilities.MAX_FILLED_SITES}" + ) + return values diff --git a/src/braket/analog_hamiltonian_simulator/rydberg/validators/device_validators/device_driving_field.py b/src/braket/analog_hamiltonian_simulator/rydberg/validators/device_validators/device_driving_field.py new file mode 100644 index 00000000..5f6f5abd --- /dev/null +++ b/src/braket/analog_hamiltonian_simulator/rydberg/validators/device_validators/device_driving_field.py @@ -0,0 +1,167 @@ +from pydantic.v1.class_validators import root_validator + +from braket.analog_hamiltonian_simulator.rydberg.validators.device_capabilities_constants import ( + DeviceCapabilitiesConstants, +) +from braket.analog_hamiltonian_simulator.rydberg.validators.driving_field import ( + DrivingFieldValidator, +) +from braket.analog_hamiltonian_simulator.rydberg.validators.field_validator_util import ( + validate_max_absolute_slope, + validate_time_precision, + validate_time_separation, + validate_value_precision, + validate_value_range_with_warning, +) +from braket.analog_hamiltonian_simulator.rydberg.validators.physical_field import PhysicalField + + +class DeviceDrivingFieldValidator(DrivingFieldValidator): + capabilities: DeviceCapabilitiesConstants + + # Amplitude must start and end at 0.0 + @root_validator(pre=True, skip_on_failure=True) + def amplitude_start_and_end_values(cls, values): + amplitude = values["amplitude"] + time_series = amplitude["time_series"] + time_series_values = time_series["values"] + if time_series_values: + start_value, end_value = time_series_values[0], time_series_values[-1] + if start_value != 0 or end_value != 0: + raise ValueError( + f"The values of the Rabi frequency at the first and last time points are " + f"{start_value}, {end_value}; they both must be nonzero." + ) + return values + + @root_validator(pre=True, skip_on_failure=True) + def amplitude_time_precision_is_correct(cls, values): + amplitude = values["amplitude"] + capabilities = values["capabilities"] + amplitude_obj = PhysicalField.parse_obj(amplitude) + validate_time_precision( + amplitude_obj.time_series.times, capabilities.GLOBAL_TIME_PRECISION, "amplitude" + ) + return values + + @root_validator(pre=True, skip_on_failure=True) + def amplitude_timepoint_not_too_close(cls, values): + amplitude = values["amplitude"] + capabilities = values["capabilities"] + validate_time_separation( + amplitude["time_series"]["times"], capabilities.GLOBAL_MIN_TIME_SEPARATION, "amplitude" + ) + return values + + @root_validator(pre=True, skip_on_failure=True) + def amplitude_value_precision_is_correct(cls, values): + amplitude = values["amplitude"] + capabilities = values["capabilities"] + amplitude_obj = PhysicalField.parse_obj(amplitude) + validate_value_precision( + amplitude_obj.time_series.values, + capabilities.GLOBAL_AMPLITUDE_VALUE_PRECISION, + "amplitude", + ) + return values + + @root_validator(pre=True, skip_on_failure=True) + def amplitude_slopes_not_too_steep(cls, values): + amplitude = values["amplitude"] + capabilities = values["capabilities"] + amplitude_times = amplitude["time_series"]["times"] + amplitude_values = amplitude["time_series"]["values"] + if amplitude_times and amplitude_values: + validate_max_absolute_slope( + amplitude_times, + amplitude_values, + capabilities.GLOBAL_AMPLITUDE_SLOPE_MAX, + "amplitude", + ) + return values + + @root_validator(pre=True, skip_on_failure=True) + def phase_time_precision_is_correct(cls, values): + phase = values["phase"] + capabilities = values["capabilities"] + phase_obj = PhysicalField.parse_obj(phase) + validate_time_precision( + phase_obj.time_series.times, capabilities.GLOBAL_TIME_PRECISION, "phase" + ) + return values + + @root_validator(pre=True, skip_on_failure=True) + def phase_timepoint_not_too_close(cls, values): + phase = values["phase"] + capabilities = values["capabilities"] + validate_time_separation( + phase["time_series"]["times"], capabilities.GLOBAL_MIN_TIME_SEPARATION, "phase" + ) + return values + + @root_validator(pre=True, skip_on_failure=True) + def phase_values_start_with_0(cls, values): + phase = values["phase"] + phase_values = phase["time_series"]["values"] + if phase_values: + if phase_values[0] != 0: + raise ValueError( + f"The first value of of driving field phase is {phase_values[0]}; it must be 0." + ) + return values + + @root_validator(pre=True, skip_on_failure=True) + def phase_values_within_range(cls, values): + phase = values["phase"] + capabilities = values["capabilities"] + validate_value_range_with_warning( + phase["time_series"]["values"], + capabilities.GLOBAL_PHASE_VALUE_MIN, + capabilities.GLOBAL_PHASE_VALUE_MAX, + "phase", + ) + return values + + @root_validator(pre=True, skip_on_failure=True) + def phase_value_precision_is_correct(cls, values): + phase = values["phase"] + capabilities = values["capabilities"] + phase_obj = PhysicalField.parse_obj(phase) + validate_value_precision( + phase_obj.time_series.values, capabilities.GLOBAL_PHASE_VALUE_PRECISION, "phase" + ) + return values + + @root_validator(pre=True, skip_on_failure=True) + def detuning_time_precision_is_correct(cls, values): + detuning = values["detuning"] + capabilities = values["capabilities"] + detuning_obj = PhysicalField.parse_obj(detuning) + validate_time_precision( + detuning_obj.time_series.times, capabilities.GLOBAL_TIME_PRECISION, "detuning" + ) + return values + + @root_validator(pre=True, skip_on_failure=True) + def detuning_timepoint_not_too_close(cls, values): + detuning = values["detuning"] + capabilities = values["capabilities"] + validate_time_separation( + detuning["time_series"]["times"], capabilities.GLOBAL_MIN_TIME_SEPARATION, "detuning" + ) + return values + + @root_validator(pre=True, skip_on_failure=True) + def detuning_slopes_not_too_steep(cls, values): + detuning = values["detuning"] + capabilities = values["capabilities"] + detuning_times = detuning["time_series"]["times"] + detuning_values = detuning["time_series"]["values"] + if detuning_times and detuning_values: + validate_max_absolute_slope( + detuning_times, + detuning_values, + capabilities.GLOBAL_DETUNING_SLOPE_MAX, + "detuning", + ) + return values diff --git a/src/braket/analog_hamiltonian_simulator/rydberg/validators/device_validators/device_hamiltonian.py b/src/braket/analog_hamiltonian_simulator/rydberg/validators/device_validators/device_hamiltonian.py new file mode 100644 index 00000000..ac9ce467 --- /dev/null +++ b/src/braket/analog_hamiltonian_simulator/rydberg/validators/device_validators/device_hamiltonian.py @@ -0,0 +1,19 @@ +from pydantic.v1.class_validators import root_validator + +from braket.analog_hamiltonian_simulator.rydberg.validators.hamiltonian import HamiltonianValidator + + +class DeviceHamiltonianValidator(HamiltonianValidator): + LOCAL_RYDBERG_CAPABILITIES: bool = False + + @root_validator(pre=True, skip_on_failure=True) + def max_zero_local_detuning(cls, values): + LOCAL_RYDBERG_CAPABILITIES = values["LOCAL_RYDBERG_CAPABILITIES"] + local_detuning = values.get("localDetuning", []) + if not LOCAL_RYDBERG_CAPABILITIES and len(local_detuning): + raise ValueError( + f"Local detuning cannot be specified; \ +{len(local_detuning)} are given. Specifying local \ +detuning is an experimental capability, use Braket Direct to request access." + ) + return values diff --git a/src/braket/analog_hamiltonian_simulator/rydberg/validators/device_validators/device_local_detuning.py b/src/braket/analog_hamiltonian_simulator/rydberg/validators/device_validators/device_local_detuning.py new file mode 100644 index 00000000..6dcb6630 --- /dev/null +++ b/src/braket/analog_hamiltonian_simulator/rydberg/validators/device_validators/device_local_detuning.py @@ -0,0 +1,97 @@ +from pydantic.v1.class_validators import root_validator + +from braket.analog_hamiltonian_simulator.rydberg.validators.device_capabilities_constants import ( + DeviceCapabilitiesConstants, +) +from braket.analog_hamiltonian_simulator.rydberg.validators.field_validator_util import ( + validate_max_absolute_slope, + validate_time_precision, + validate_time_separation, +) +from braket.analog_hamiltonian_simulator.rydberg.validators.local_detuning import ( + LocalDetuningValidator, +) +from braket.analog_hamiltonian_simulator.rydberg.validators.physical_field import PhysicalField + + +class DeviceLocalDetuningValidator(LocalDetuningValidator): + capabilities: DeviceCapabilitiesConstants + + @root_validator(pre=True, skip_on_failure=True) + def check_local_rydberg_capabilities(cls, values): + capabilities = values["capabilities"] + if not capabilities.LOCAL_RYDBERG_CAPABILITIES: + raise ValueError( + "Local Rydberg capabilities information has not been " + "provided for local detuning." + ) + return values + + # Rule: The number of locally-addressed sites must not exceed + # rydberg.local.number_local_detuning_sites + @root_validator(pre=True, skip_on_failure=True) + def magnitude_pattern_have_not_too_many_nonzeros(cls, values): + magnitude = values["magnitude"] + capabilities = values["capabilities"] + pattern = magnitude["pattern"] + num_nonzeros = sum([p != 0.0 for p in pattern]) + if num_nonzeros > capabilities.LOCAL_MAX_NONZERO_PATTERN_VALUES: + raise ValueError( + f"Number of nonzero magnitude pattern values is {num_nonzeros}; " + f"it must not be more than {capabilities.LOCAL_MAX_NONZERO_PATTERN_VALUES}" + ) + return values + + # Rule: The Rydberg local detuning times have a + # resolution of at least rydberg.local.time_resolution + @root_validator(pre=True, skip_on_failure=True) + def magnitude_time_precision_is_correct(cls, values): + magnitude = values["magnitude"] + capabilities = values["capabilities"] + magnitude_obj = PhysicalField.parse_obj(magnitude) + validate_time_precision( + magnitude_obj.time_series.times, capabilities.LOCAL_TIME_PRECISION, "magnitude" + ) + return values + + # Rule: The Rydberg local detuning times must be spaced at least rydberg.local.time_delta_min + @root_validator(pre=True, skip_on_failure=True) + def magnitude_timepoint_not_too_close(cls, values): + magnitude = values["magnitude"] + capabilities = values["capabilities"] + times = magnitude["time_series"]["times"] + validate_time_separation(times, capabilities.LOCAL_MIN_TIME_SEPARATION, "magnitude") + return values + + # Rule: The Rydberg local detuning slew rate must + # not exceed rydberg.local.detuning_slew_rate_max + @root_validator(pre=True, skip_on_failure=True) + def magnitude_slopes_not_too_steep(cls, values): + magnitude = values["magnitude"] + capabilities = values["capabilities"] + magnitude_times = magnitude["time_series"]["times"] + magnitude_values = magnitude["time_series"]["values"] + if magnitude_times and magnitude_values: + validate_max_absolute_slope( + magnitude_times, + magnitude_values, + capabilities.LOCAL_MAGNITUDE_SLOPE_MAX, + "magnitude", + ) + return values + + # Rule: The Rydberg local detuning must start and end at 0. + @root_validator(pre=True, skip_on_failure=True) + def local_detuning_start_and_end_values(cls, values): + magnitude = values["magnitude"] + time_series = magnitude["time_series"] + time_series_values = time_series["values"] + if time_series_values: + start_value, end_value = time_series_values[0], time_series_values[-1] + if start_value != 0 or end_value != 0: + raise ValueError( + f"The values of the shifting field magnitude time series at the first " + f"and last time points are {start_value}, {end_value}; " + f"they both must be nonzero." + ) + return values diff --git a/src/braket/analog_hamiltonian_simulator/rydberg/validators/field_validator_util.py b/src/braket/analog_hamiltonian_simulator/rydberg/validators/field_validator_util.py index e34fadea..aefb1ce5 100644 --- a/src/braket/analog_hamiltonian_simulator/rydberg/validators/field_validator_util.py +++ b/src/braket/analog_hamiltonian_simulator/rydberg/validators/field_validator_util.py @@ -104,3 +104,50 @@ def validate_net_detuning_with_warning( # Return immediately if there is an atom has net detuning # exceeding MAX_NET_DETUNING at a time point return program + + +# Two time points cannot be too close, assuming the time points are sorted ascendingly +def validate_time_separation(times: List[Decimal], min_time_separation: Decimal, name: str): + for i in range(len(times) - 1): + time_diff = times[i + 1] - times[i] + if time_diff < min_time_separation: + raise ValueError( + f"Time points of {name} time_series, {i} ({times[i]}) and " + f"{i + 1} ({times[i + 1]}), are too close; they are separated " + f"by {time_diff} seconds. It must be at least {min_time_separation} seconds" + ) + + +def validate_value_precision(values: List[Decimal], max_precision: Decimal, name: str): + # Raise ValueError if at any item in the values is beyond the max allowable precision + for idx, v in enumerate(values): + if v % max_precision != 0: + raise ValueError( + f"Value {idx} ({v}) in {name} time_series is defined with too many digits; " + f"it must be an integer multiple of {max_precision}" + ) + + +def validate_max_absolute_slope( + times: List[Decimal], values: List[Decimal], max_slope: Decimal, name: str +): + # Raise ValueError if at any time the time series (times, values) + # rises/falls faster than allowed + for idx in range(len(values) - 1): + slope = (values[idx + 1] - values[idx]) / (times[idx + 1] - times[idx]) + if abs(slope) > max_slope: + raise ValueError( + f"For the {name} field, rate of change of values " + f"(between the {idx}-th and the {idx + 1}-th times) " + f"is {abs(slope)}, more than {max_slope}" + ) + + +def validate_time_precision(times: List[Decimal], time_precision: Decimal, name: str): + for idx, t in enumerate(times): + if t % time_precision != 0: + raise ValueError( + f"time point {idx} ({t}) of {name} time_series is " + f"defined with too many digits; it must be an " + f"integer multiple of {time_precision}" + ) diff --git a/src/braket/analog_hamiltonian_simulator/rydberg/validators/program.py b/src/braket/analog_hamiltonian_simulator/rydberg/validators/program.py index e7afb004..86d7c378 100644 --- a/src/braket/analog_hamiltonian_simulator/rydberg/validators/program.py +++ b/src/braket/analog_hamiltonian_simulator/rydberg/validators/program.py @@ -12,6 +12,7 @@ # language governing permissions and limitations under the License. from copy import deepcopy +from typing import Union from braket.ir.ahs.program_v1 import Program from pydantic.v1 import root_validator @@ -20,13 +21,16 @@ from braket.analog_hamiltonian_simulator.rydberg.validators.capabilities_constants import ( CapabilitiesConstants, ) +from braket.analog_hamiltonian_simulator.rydberg.validators.device_capabilities_constants import ( + DeviceCapabilitiesConstants, +) from braket.analog_hamiltonian_simulator.rydberg.validators.field_validator_util import ( validate_net_detuning_with_warning, ) class ProgramValidator(Program): - capabilities: CapabilitiesConstants + capabilities: Union[CapabilitiesConstants, DeviceCapabilitiesConstants] # The pattern of the shifting field must have the same length as the lattice_sites @root_validator(pre=True, skip_on_failure=True) @@ -61,7 +65,7 @@ def net_detuning_must_not_exceed_max_net_detuning(cls, values): # If no local detuning, we simply return the values # because there are separate validators to validate # the global driving fields in the program - if not len(local_detuning): + if not len(local_detuning) or not capabilities.MAX_NET_DETUNING: return values detuning_times = [ diff --git a/test/unit_tests/braket/analog_hamiltonian_simulator/test_validator/validators/device_validators/conftest.py b/test/unit_tests/braket/analog_hamiltonian_simulator/test_validator/validators/device_validators/conftest.py new file mode 100644 index 00000000..e311c56e --- /dev/null +++ b/test/unit_tests/braket/analog_hamiltonian_simulator/test_validator/validators/device_validators/conftest.py @@ -0,0 +1,65 @@ +from decimal import Decimal + +import pytest + +from braket.analog_hamiltonian_simulator.rydberg.validators.device_capabilities_constants import ( + DeviceCapabilitiesConstants, +) + + +@pytest.fixture +def non_local_capabilities_constants(): + capabilities_dict = { + "BOUNDING_BOX_SIZE_X": Decimal("0.000075"), + "BOUNDING_BOX_SIZE_Y": Decimal("0.000076"), + "DIMENSIONS": 2, + "GLOBAL_AMPLITUDE_SLOPE_MAX": Decimal("250000000000000"), + "GLOBAL_AMPLITUDE_VALUE_MAX": Decimal("15800000.0"), + "GLOBAL_AMPLITUDE_VALUE_MIN": Decimal("0.0"), + "GLOBAL_AMPLITUDE_VALUE_PRECISION": Decimal("400.0"), + "GLOBAL_DETUNING_SLOPE_MAX": Decimal("250000000000000"), + "GLOBAL_DETUNING_VALUE_MAX": Decimal("125000000.0"), + "GLOBAL_DETUNING_VALUE_MIN": Decimal("-125000000.0"), + "GLOBAL_DETUNING_VALUE_PRECISION": Decimal("0.2"), + "GLOBAL_MIN_TIME_SEPARATION": Decimal("1E-8"), + "GLOBAL_PHASE_VALUE_MAX": Decimal("99.0"), + "GLOBAL_PHASE_VALUE_MIN": Decimal("-99.0"), + "GLOBAL_PHASE_VALUE_PRECISION": Decimal("5E-7"), + "GLOBAL_TIME_PRECISION": Decimal("1E-9"), + "LOCAL_MAGNITUDE_SEQUENCE_VALUE_MAX": None, + "LOCAL_MAGNITUDE_SEQUENCE_VALUE_MIN": None, + "LOCAL_MAGNITUDE_SLOPE_MAX": None, + "LOCAL_MIN_DISTANCE_BETWEEN_SHIFTED_SITES": None, + "LOCAL_MIN_TIME_SEPARATION": None, + "LOCAL_RYDBERG_CAPABILITIES": False, + "LOCAL_TIME_PRECISION": None, + "MAGNITUDE_PATTERN_VALUE_MAX": None, + "MAGNITUDE_PATTERN_VALUE_MIN": None, + "MAX_FILLED_SITES": 4, + "MAX_NET_DETUNING": None, + "MAX_SITES": 8, + "MAX_TIME": Decimal("0.000004"), + "MIN_DISTANCE": Decimal("0.000004"), + "MIN_ROW_DISTANCE": Decimal("0.000004"), + "SITE_PRECISION": Decimal("1E-7"), + } + return DeviceCapabilitiesConstants(**capabilities_dict) + + +@pytest.fixture +def capabilities_with_local_rydberg(non_local_capabilities_constants): + local_rydberg_constants = { + "LOCAL_RYDBERG_CAPABILITIES": True, + "LOCAL_TIME_PRECISION": Decimal("1e-9"), + "LOCAL_MAGNITUDE_SEQUENCE_VALUE_MAX": Decimal("125000000.0"), + "LOCAL_MAGNITUDE_SEQUENCE_VALUE_MIN": Decimal("0.0"), + "LOCAL_MAGNITUDE_SLOPE_MAX": Decimal("1256600000000000.0"), + "LOCAL_MIN_DISTANCE_BETWEEN_SHIFTED_SITES": Decimal("5e-06"), + "LOCAL_MIN_TIME_SEPARATION": Decimal("1E-8"), + "LOCAL_MAX_NONZERO_PATTERN_VALUES": 200, + "MAGNITUDE_PATTERN_VALUE_MAX": Decimal("1.0"), + "MAGNITUDE_PATTERN_VALUE_MIN": Decimal("0.0"), + } + capabilities_dict = non_local_capabilities_constants.dict() + capabilities_dict.update(local_rydberg_constants) + return DeviceCapabilitiesConstants(**capabilities_dict) diff --git a/test/unit_tests/braket/analog_hamiltonian_simulator/test_validator/validators/device_validators/test_device_atom_arrangement.py b/test/unit_tests/braket/analog_hamiltonian_simulator/test_validator/validators/device_validators/test_device_atom_arrangement.py new file mode 100644 index 00000000..ee309630 --- /dev/null +++ b/test/unit_tests/braket/analog_hamiltonian_simulator/test_validator/validators/device_validators/test_device_atom_arrangement.py @@ -0,0 +1,172 @@ +from decimal import Decimal + +import pytest +from pydantic.v1.error_wrappers import ValidationError + +from braket.analog_hamiltonian_simulator.rydberg.validators.atom_arrangement import AtomArrangement +from braket.analog_hamiltonian_simulator.rydberg.validators.device_validators import ( + DeviceAtomArrangementValidator, +) + + +@pytest.fixture +def atom_arrangement_data(): + return { + "sites": [[0, 0], [0, 5e-6], [5e-6, 0], [5e-6, 10e-6]], + "filling": [1, 0, 1, 0], + } + + +@pytest.fixture +def mock_atom_arrangement_data(): + data = { + "sites": [], + "filling": [], + } + return AtomArrangement.parse_obj(data).dict() + + +@pytest.mark.parametrize( + "sites, filling, error_message", + [ + ( + [], + [], + "Sites can not be empty.", + ), + ( + [], + [1, 0, 1, 0], + "Filling length (4) does not match sites length (0)", + ), + ( + [ + [Decimal("0.0"), Decimal("0.0")], + [Decimal("0.0"), Decimal("4e-6")], + [Decimal("5e-6"), Decimal("0.0")], + [Decimal("5e-6"), Decimal("4e-6")], + ], + [], + "Filling length (0) does not match sites length (4)", + ), + ], +) +def test_atom_arrangement_sites_or_fillings_empty( + sites, filling, error_message, mock_atom_arrangement_data, capabilities_with_local_rydberg +): + mock_atom_arrangement_data["sites"] = sites + mock_atom_arrangement_data["filling"] = filling + _assert_atom_arrangement( + mock_atom_arrangement_data, error_message, capabilities_with_local_rydberg + ) + + +def test_valid_atom_array(atom_arrangement_data, capabilities_with_local_rydberg): + try: + DeviceAtomArrangementValidator( + capabilities=capabilities_with_local_rydberg, **atom_arrangement_data + ) + except ValidationError as e: + pytest.fail(f"Validate test is failing : {str(e)}") + + +@pytest.mark.parametrize( + "sites, filling ,error_message", + [ + ( + [[1.1e-6, 1.1e-6], [2.2e-6, 7.31e-6]], + [1, 0], + "Coordinates 1([2.2e-06, 7.31e-06]) is defined with too high precision;" + "they must be multiples of 1E-7 meters", + ), + ( + [[3.201e-6, 0.0], [0.00, 4.1e-6], [-1.101e-6, 8.11e-6]], + [1, 0, 1], + "Coordinates 0([3.201e-06, 0.0]) is defined with too high precision;" + "they must be multiples of 1E-7 meters", + ), + ], +) +# Rule: Lattice coordinates have a maximum precision of lattice.geometry.position_resolution +def test_atom_arrangement_sites_defined_with_right_precision( + sites, filling, error_message, mock_atom_arrangement_data, non_local_capabilities_constants +): + mock_atom_arrangement_data["sites"] = sites + mock_atom_arrangement_data["filling"] = filling + _assert_atom_arrangement( + mock_atom_arrangement_data, error_message, non_local_capabilities_constants + ) + + +def _assert_atom_arrangement(data, error_message, device_capabilities_constants): + with pytest.raises(ValidationError) as e: + DeviceAtomArrangementValidator(capabilities=device_capabilities_constants, **data) + assert error_message in str(e.value) + + +def test_atom_arrangement_sites_not_too_many( + mock_atom_arrangement_data, non_local_capabilities_constants +): + sites = [[0.0, 0.0]] * (non_local_capabilities_constants.MAX_SITES + 1) + filling = [0] * (non_local_capabilities_constants.MAX_SITES + 1) + mock_atom_arrangement_data["sites"] = sites + mock_atom_arrangement_data["filling"] = filling + error_message = "There are too many sites (9); there must be at most 8 sites" + _assert_atom_arrangement( + mock_atom_arrangement_data, error_message, non_local_capabilities_constants + ) + + +@pytest.mark.parametrize( + "sites, filling,error_message", + [ + ( + [[Decimal("0.0"), Decimal("0.0")], [Decimal("5e-6"), Decimal("2.4e-6")]], + [0, 1], + "Sites [Decimal('0.0'), Decimal('0.0')] and site [Decimal('0.000005'), " + "Decimal('0.0000024')] have y-separation (0.0000024). It must either be " + "exactly zero or not smaller than 0.000004 meters", + ), + ( + [[Decimal("0.0"), Decimal("0.0")], [Decimal("5e-6"), Decimal("1.4e-6")]], + [0, 1], + "Sites [Decimal('0.0'), Decimal('0.0')] and site [Decimal('0.000005'), " + "Decimal('0.0000014')] have y-separation (0.0000014). It must either " + "be exactly zero or not smaller than 0.000004 meters", + ), + ], +) +# Rule: All sites in the lattice must be separated vertically +# by at least lattice.geometry.spacing_vertical_min +def test_atom_arrangement_sites_in_rows( + sites, + filling, + error_message, + mock_atom_arrangement_data, + non_local_capabilities_constants, +): + mock_atom_arrangement_data["sites"] = sites + mock_atom_arrangement_data["filling"] = filling + _assert_atom_arrangement( + mock_atom_arrangement_data, + error_message, + non_local_capabilities_constants, + ) + + +def test_atom_arrangement_filling_atom_number_limit( + mock_atom_arrangement_data, non_local_capabilities_constants +): + filling = [1] * (non_local_capabilities_constants.MAX_FILLED_SITES + 1) + mock_atom_arrangement_data["filling"] = filling + mock_atom_arrangement_data["sites"] = [ + [0, 0], + [0, 0.00004], + [0, 0.00005], + [0.00005, 0.00004], + [0.00005, 0], + ] + error_message = "Filling has 5 '1' entries; it must have not more than 4" + _assert_atom_arrangement( + mock_atom_arrangement_data, error_message, non_local_capabilities_constants + ) diff --git a/test/unit_tests/braket/analog_hamiltonian_simulator/test_validator/validators/device_validators/test_device_driving_field.py b/test/unit_tests/braket/analog_hamiltonian_simulator/test_validator/validators/device_validators/test_device_driving_field.py new file mode 100644 index 00000000..939afcbb --- /dev/null +++ b/test/unit_tests/braket/analog_hamiltonian_simulator/test_validator/validators/device_validators/test_device_driving_field.py @@ -0,0 +1,321 @@ +import pytest +from pydantic.v1.error_wrappers import ValidationError + +from braket.analog_hamiltonian_simulator.rydberg.validators.device_validators import ( + DeviceDrivingFieldValidator, +) +from braket.analog_hamiltonian_simulator.rydberg.validators.driving_field import DrivingField + + +@pytest.fixture +def driving_field_data(): + return { + "amplitude": { + "pattern": "uniform", + "time_series": { + "times": [0, 1e-07, 3.9e-06, 4e-06], + "values": [0, 12566400.0, 12566400.0, 0], + }, + }, + "phase": { + "pattern": "uniform", + "time_series": { + "times": [0, 1e-07, 3.9e-06, 4e-06], + "values": [0, 0, -16.0832, -16.0832], + }, + }, + "detuning": { + "pattern": "uniform", + "time_series": { + "times": [0, 1e-07, 3.9e-06, 4e-06], + "values": [-125000000, -125000000, 125000000, 125000000], + }, + }, + } + + +@pytest.fixture +def mock_driving_field_data(): + data = { + "amplitude": { + "pattern": "uniform", + "time_series": { + "times": [], + "values": [], + }, + }, + "phase": { + "pattern": "uniform", + "time_series": { + "times": [], + "values": [], + }, + }, + "detuning": { + "pattern": "uniform", + "time_series": { + "times": [], + "values": [], + }, + }, + } + return DrivingField.parse_obj(data).dict() + + +def test_device_driving_field(driving_field_data, non_local_capabilities_constants): + try: + DeviceDrivingFieldValidator( + capabilities=non_local_capabilities_constants, **driving_field_data + ) + except ValidationError as e: + pytest.fail(f"Validate test is failing : {str(e)}") + + +@pytest.mark.parametrize( + "times, field_name, error_message", + [ + ( + [0.0, 12.1e-9], + "amplitude", + "time point 1 (1.21E-8) of amplitude time_series is " + "defined with too many digits; it must be an integer multiple of 1E-9", + ), + ( + [0.0, 12.1e-9], + "phase", + "time point 1 (1.21E-8) of phase time_series is " + "defined with too many digits; it must be an integer multiple of 1E-9", + ), + ( + [0.0, 12.1e-9], + "detuning", + "time point 1 (1.21E-8) of detuning time_series is " + "defined with too many digits; it must be an integer multiple of 1E-9", + ), + ( + [0.0, 12.1e-9, 4e-6], + "amplitude", + "time point 1 (1.21E-8) of amplitude time_series is " + "defined with too many digits; it must be an integer multiple of 1E-9", + ), + ( + [0.0, 12.1e-9, 4e-6], + "phase", + "time point 1 (1.21E-8) of phase time_series is " + "defined with too many digits; it must be an integer multiple of 1E-9", + ), + ( + [0.0, 12.1e-9, 4e-6], + "detuning", + "time point 1 (1.21E-8) of detuning time_series is " + "defined with too many digits; it must be an integer multiple of 1E-9", + ), + ( + [0.0, 12.1e-9, 22.1e-9], + "amplitude", + "time point 1 (1.21E-8) of amplitude time_series is " + "defined with too many digits; it must be an integer multiple of 1E-9", + ), + ( + [0.0, 12.1e-9, 22.1e-9], + "phase", + "time point 1 (1.21E-8) of phase time_series is " + "defined with too many digits; it must be an integer multiple of 1E-9", + ), + ( + [0.0, 12.1e-9, 22.1e-9], + "detuning", + "time point 1 (1.21E-8) of detuning time_series is " + "defined with too many digits; it must be an integer multiple of 1E-9", + ), + ( + [0.0, 22.1e-9, 12.1e-9], + "amplitude", + "time point 1 (2.21E-8) of amplitude time_series is " + "defined with too many digits; it must be an integer multiple of 1E-9", + ), + ( + [0.0, 22.1e-9, 12.1e-9], + "phase", + "time point 1 (2.21E-8) of phase time_series is " + "defined with too many digits; it must be an integer multiple of 1E-9", + ), + ( + [0.0, 22.1e-9, 12.1e-9], + "detuning", + "time point 1 (2.21E-8) of detuning time_series is " + "defined with too many digits; it must be an integer multiple of 1E-9", + ), + ], +) +# Rule: The times for any component of the effective Hamiltonian +# have a maximum precision of rydberg.global.time_resolution +def test_driving_field_time_precision_is_correct( + times, field_name, error_message, mock_driving_field_data, non_local_capabilities_constants +): + mock_driving_field_data[field_name]["time_series"]["times"] = times + _assert_driving_field(mock_driving_field_data, error_message, non_local_capabilities_constants) + + +def test_driving_field_no_detuning(mock_driving_field_data, non_local_capabilities_constants): + mock_driving_field_data["detuning"]["time_series"]["times"].clear() + mock_driving_field_data["detuning"]["time_series"]["values"].clear() + try: + DeviceDrivingFieldValidator( + capabilities=non_local_capabilities_constants, **mock_driving_field_data + ) + except ValidationError as e: + pytest.fail(f"Validate test is failing : {str(e)}") + + +@pytest.mark.parametrize( + "values, field_name, error_message", + [ + ( + [0.0, 22.1e-9, 12, 1e-9, 0.0], + "amplitude", + "Value 1 (2.21E-8) in amplitude time_series is " + "defined with too many digits; it must be an integer multiple of 400.0", + ) + ], +) +def test_driving_field_value_precision_is_correct( + values, field_name, error_message, driving_field_data, non_local_capabilities_constants +): + driving_field_data[field_name]["time_series"]["values"] = values + _assert_driving_field(driving_field_data, error_message, non_local_capabilities_constants) + + +@pytest.mark.parametrize( + "times, field_name, error_message", + [ + ( + [0.0, 9e-9, 25e-9], + "amplitude", + "Time points of amplitude time_series, 0 (0.0) and 1 (9e-09), are too close; " + "they are separated by 9e-09 seconds. It must be at least 1E-8 seconds", + ), + ( + [0.0, 9e-9, 25e-9], + "phase", + "Time points of phase time_series, 0 (0.0) and 1 (9e-09), are too close; " + "they are separated by 9e-09 seconds. It must be at least 1E-8 seconds", + ), + ( + [0.0, 9e-9, 25e-9], + "detuning", + "Time points of detuning time_series, 0 (0.0) and 1 (9e-09), are too close; " + "they are separated by 9e-09 seconds. It must be at least 1E-8 seconds", + ), + ( + [0.0, 9e-9, 25e-9, 30e-9], + "amplitude", + "Time points of amplitude time_series, 0 (0.0) and 1 (9e-09), are too close; " + "they are separated by 9e-09 seconds. It must be at least 1E-8 seconds", + ), + ( + [0.0, 9e-9, 25e-9, 30e-9], + "phase", + "Time points of phase time_series, 0 (0.0) and 1 (9e-09), are too close; " + "they are separated by 9e-09 seconds. It must be at least 1E-8 seconds", + ), + ( + [0.0, 9e-9, 25e-9, 30e-9], + "detuning", + "Time points of detuning time_series, 0 (0.0) and 1 (9e-09), are too close; " + "they are separated by 9e-09 seconds. It must be at least 1E-8 seconds", + ), + ], +) +# Rule: The times for any component of the effective Hamiltonian +# must be spaced by at least rydberg.global.time_delta_min +def test_driving_field_timepoint_not_too_close( + times, field_name, error_message, mock_driving_field_data, non_local_capabilities_constants +): + mock_driving_field_data[field_name]["time_series"]["times"] = times + _assert_driving_field(mock_driving_field_data, error_message, non_local_capabilities_constants) + + +@pytest.mark.parametrize( + "values, field_name, error_message", + [ + ( + [2.5e7, 2.5e7, 2.5e7, 0.0], + "amplitude", + "The values of the Rabi frequency at the first and last time points " + "are 25000000.0, 0.0; they both must be nonzero.", + ), + ( + [0.0, 2.5e7, 2.5e7, 2.5e7], + "amplitude", + "The values of the Rabi frequency at the first and last time points " + "are 0.0, 25000000.0; they both must be nonzero.", + ), + ( + [2.5e7, 2.5e7, 2.5e7, 2.5e7], + "amplitude", + "The values of the Rabi frequency at the first and last time points " + "are 25000000.0, 25000000.0; they both must be nonzero.", + ), + ], +) +# Rule: The global Rydberg Rabi frequency amplitude must start and end at 0. +def test_driving_field_amplitude_start_and_end_values( + values, field_name, error_message, mock_driving_field_data, non_local_capabilities_constants +): + mock_driving_field_data[field_name]["time_series"]["values"] = values + _assert_driving_field(mock_driving_field_data, error_message, non_local_capabilities_constants) + + +@pytest.mark.parametrize( + "values, times, field_name, error_message", + [ + ( + [0.0, 2.5e7, 0.0], + [0.0, 0.01e-6, 2.0e-6], + "amplitude", + "For the amplitude field, rate of change of values " + "(between the 0-th and the 1-th times) " + "is 2500000000000000.0, more than 250000000000000", + ), + ( + [0.0, 2.5e7, 2.5e7, 0.0], + [0.0, 0.01e-6, 3.2e-6, 3.21e-6], + "amplitude", + "For the amplitude field, rate of change of values " + "(between the 0-th and the 1-th times) " + "is 2500000000000000.0, more than 250000000000000", + ), + ( + [0.0, 2.5e7, 2.5e7, 0.0], + [0.0, 0.01e-6, 3.2e-6, 3.21e-6], + "detuning", + "For the detuning field, rate of change of values " + "(between the 0-th and the 1-th times) " + "is 2500000000000000.0, more than 250000000000000", + ), + ], +) +def test_driving_field_slopes_not_too_steep( + values, + times, + field_name, + error_message, + mock_driving_field_data, + non_local_capabilities_constants, +): + mock_driving_field_data[field_name]["time_series"]["values"] = values + mock_driving_field_data[field_name]["time_series"]["times"] = times + _assert_driving_field(mock_driving_field_data, error_message, non_local_capabilities_constants) + + +def test_phase_values_start_with_0(mock_driving_field_data, non_local_capabilities_constants): + mock_driving_field_data["phase"]["time_series"]["values"] = [0.1, 2.5e7] + error_message = "The first value of of driving field phase is 0.1; it must be 0." + _assert_driving_field(mock_driving_field_data, error_message, non_local_capabilities_constants) + + +def _assert_driving_field(data, error_message, device_capabilities_constants): + with pytest.raises(ValidationError) as e: + DeviceDrivingFieldValidator(capabilities=device_capabilities_constants, **data) + assert error_message in str(e.value) diff --git a/test/unit_tests/braket/analog_hamiltonian_simulator/test_validator/validators/device_validators/test_device_hamiltonian.py b/test/unit_tests/braket/analog_hamiltonian_simulator/test_validator/validators/device_validators/test_device_hamiltonian.py new file mode 100644 index 00000000..9f73f417 --- /dev/null +++ b/test/unit_tests/braket/analog_hamiltonian_simulator/test_validator/validators/device_validators/test_device_hamiltonian.py @@ -0,0 +1,72 @@ +import pytest +from pydantic.v1.error_wrappers import ValidationError + +from braket.analog_hamiltonian_simulator.rydberg.validators.device_validators import ( + DeviceHamiltonianValidator, +) + + +@pytest.fixture +def hamiltonian_data(): + return { + "drivingFields": [ + { + "amplitude": { + "pattern": "uniform", + "time_series": { + "times": [0, 1e-07, 3.9e-06, 4e-06], + "values": [0, 12566400.0, 12566400.0, 0], + }, + }, + "phase": { + "pattern": "uniform", + "time_series": { + "times": [0, 1e-07, 3.9e-06, 4e-06], + "values": [0, 0, -16.0832, -16.0832], + }, + }, + "detuning": { + "pattern": "uniform", + "time_series": { + "times": [0, 1e-07, 3.9e-06, 4e-06], + "values": [-125000000, -125000000, 125000000, 125000000], + }, + }, + } + ], + "localDetuning": [ + { + "magnitude": { + "time_series": {"times": [0, 4e-6], "values": [0, 0]}, + "pattern": [0.0, 1.0, 0.5, 0.0, 1.0], + } + } + ], + } + + +def test_hamiltonian(hamiltonian_data): + try: + DeviceHamiltonianValidator(**hamiltonian_data, LOCAL_RYDBERG_CAPABILITIES=True) + except ValidationError as e: + pytest.fail(f"Validate test is failing : {str(e)}") + + +@pytest.mark.parametrize("local_rydberg_exists", [(True), (False)]) +def test_hamiltonian_no_detuning(local_rydberg_exists, hamiltonian_data): + hamiltonian_data["localDetuning"].clear() + try: + DeviceHamiltonianValidator( + **hamiltonian_data, LOCAL_RYDBERG_CAPABILITIES=local_rydberg_exists + ) + except ValidationError as e: + pytest.fail(f"Validate test is failing : {str(e)}") + + +def test_no_local_rydberg_capabilities(hamiltonian_data): + error_message = "Local detuning cannot be specified; \ +1 are given. Specifying local \ +detuning is an experimental capability, use Braket Direct to request access." + with pytest.raises(ValidationError) as e: + DeviceHamiltonianValidator(**hamiltonian_data, LOCAL_RYDBERG_CAPABILITIES=False) + assert error_message in str(e.value) diff --git a/test/unit_tests/braket/analog_hamiltonian_simulator/test_validator/validators/device_validators/test_device_ir_validator.py b/test/unit_tests/braket/analog_hamiltonian_simulator/test_validator/validators/device_validators/test_device_ir_validator.py new file mode 100644 index 00000000..3c4d4e9d --- /dev/null +++ b/test/unit_tests/braket/analog_hamiltonian_simulator/test_validator/validators/device_validators/test_device_ir_validator.py @@ -0,0 +1,13 @@ +import pytest +from pydantic.v1.error_wrappers import ValidationError + +from braket.analog_hamiltonian_simulator.rydberg.validators.device_ir_validator import ( + validate_program, +) + + +def test_validate_program(program_data, capabilities_with_local_rydberg): + try: + validate_program(program=program_data, device_capabilities=capabilities_with_local_rydberg) + except ValidationError as e: + pytest.fail(f"Validate program is failing : {str(e)}") diff --git a/test/unit_tests/braket/analog_hamiltonian_simulator/test_validator/validators/device_validators/test_device_local_detuning.py b/test/unit_tests/braket/analog_hamiltonian_simulator/test_validator/validators/device_validators/test_device_local_detuning.py new file mode 100644 index 00000000..6d2153e4 --- /dev/null +++ b/test/unit_tests/braket/analog_hamiltonian_simulator/test_validator/validators/device_validators/test_device_local_detuning.py @@ -0,0 +1,169 @@ +import pytest +from braket.ir.ahs.local_detuning import LocalDetuning +from pydantic.v1.error_wrappers import ValidationError + +from braket.analog_hamiltonian_simulator.rydberg.validators.device_validators import ( + DeviceLocalDetuningValidator, +) + + +@pytest.fixture +def local_detuning_data(): + return { + "magnitude": { + "time_series": {"times": [0, 1e-07], "values": [0, 0]}, + "pattern": [0.0, 0.1, 0.1, 0.0, 0.1], + } + } + + +@pytest.fixture +def mock_local_detuning_data(): + data = { + "magnitude": { + "pattern": [], + "time_series": { + "times": [], + "values": [], + }, + } + } + return LocalDetuning.parse_obj(data).dict() + + +def test_valid_detuning(local_detuning_data, capabilities_with_local_rydberg): + try: + DeviceLocalDetuningValidator( + capabilities=capabilities_with_local_rydberg, **local_detuning_data + ) + except ValidationError as e: + pytest.fail(f"Validate test is failing: {str(e.value)}") + + +def test_validation_no_time_series(mock_local_detuning_data, capabilities_with_local_rydberg): + mock_local_detuning_data["magnitude"]["time_series"]["times"].clear() + mock_local_detuning_data["magnitude"]["time_series"]["values"].clear() + + try: + DeviceLocalDetuningValidator( + capabilities=capabilities_with_local_rydberg, **mock_local_detuning_data + ) + except ValidationError as e: + pytest.fail(f"Validate test is failing: {str(e.value)}") + + +def test_validation_no_detuning_data(mock_local_detuning_data, non_local_capabilities_constants): + error_message = ( + "Local Rydberg capabilities information has not been " "provided for local detuning." + ) + non_local_capabilities_constants.LOCAL_RYDBERG_CAPABILITIES = False + with pytest.raises(ValueError) as e: + DeviceLocalDetuningValidator( + capabilities=non_local_capabilities_constants, **mock_local_detuning_data + ) + assert error_message in str(e.value) + + +def test_shifting_field_magnitude_pattern_have_not_too_many_nonzeros( + mock_local_detuning_data, capabilities_with_local_rydberg +): + pattern = [0.1] * 257 + mock_local_detuning_data["magnitude"]["pattern"] = pattern + error_message = ( + "Number of nonzero magnitude pattern values is 257; it must not be more than 200" + ) + _assert_local_detuning(mock_local_detuning_data, error_message, capabilities_with_local_rydberg) + + +@pytest.mark.parametrize( + "values, times, error_message", + [ + ( + [0.0, 2.5e7, 0.0], + [0.0, 0.01e-6, 2.0e-6], + "For the magnitude field, rate of change of values " + "(between the 0-th and the 1-th times) " + "is 2500000000000000.0, more than 1256600000000000.0", + ), + ( + [0.0, 2.5e7, 2.5e7, 0.0], + [0.0, 0.01e-6, 3.2e-6, 3.21e-6], + "For the magnitude field, rate of change of values " + "(between the 0-th and the 1-th times) " + "is 2500000000000000.0, more than 1256600000000000.0", + ), + ], +) +def test_local_detuning_slopes_not_too_steep( + values, times, error_message, mock_local_detuning_data, capabilities_with_local_rydberg +): + mock_local_detuning_data["magnitude"]["time_series"]["values"] = values + mock_local_detuning_data["magnitude"]["time_series"]["times"] = times + _assert_local_detuning(mock_local_detuning_data, error_message, capabilities_with_local_rydberg) + + +@pytest.mark.parametrize( + "times, error_message", + [ + ( + [0.0, 12.1e-9], + "time point 1 (1.21E-8) of magnitude time_series is " + "defined with too many digits; it must be an integer multiple of 1E-9", + ), + ( + [0.0, 12.1e-9, 4e-6], + "time point 1 (1.21E-8) of magnitude time_series is " + "defined with too many digits; it must be an integer multiple of 1E-9", + ), + ( + [0.0, 12.1e-9, 22.1e-9], + "time point 1 (1.21E-8) of magnitude time_series is " + "defined with too many digits; it must be an integer multiple of 1E-9", + ), + ( + [0.0, 22.1e-9, 12.1e-9], + "time point 1 (2.21E-8) of magnitude time_series is " + "defined with too many digits; it must be an integer multiple of 1E-9", + ), + ], +) +# Rule: The times for any component of the effective Hamiltonian have +# a maximum precision of rydberg.global.time_resolution +def test_shifting_field_magnitude_time_precision_is_correct( + times, error_message, mock_local_detuning_data, capabilities_with_local_rydberg +): + mock_local_detuning_data["magnitude"]["time_series"]["times"] = times + _assert_local_detuning(mock_local_detuning_data, error_message, capabilities_with_local_rydberg) + + +@pytest.mark.parametrize( + "times, error_message", + [ + ( + [0.0, 1.0, 2.0], + "The values of the shifting field magnitude time series at " + "the first and last time points are 0.0, 2.0; they both must be nonzero.", + ), + ( + [0.2, 1.0, 0], + "The values of the shifting field magnitude time series at " + "the first and last time points are 0.2, 0; they both must be nonzero.", + ), + ( + [0.0, 0.0, 1e-5], + "The values of the shifting field magnitude time series at " + "the first and last time points are 0.0, 1e-05; they both must be nonzero.", + ), + ], +) +def test_shifting_start_and_end_are_zero( + times, error_message, mock_local_detuning_data, capabilities_with_local_rydberg +): + mock_local_detuning_data["magnitude"]["time_series"]["values"] = times + _assert_local_detuning(mock_local_detuning_data, error_message, capabilities_with_local_rydberg) + + +def _assert_local_detuning(data, error_message, device_capabilities_constants): + with pytest.raises(ValidationError) as e: + DeviceLocalDetuningValidator(capabilities=device_capabilities_constants, **data) + assert error_message in str(e.value)