|
|
|
|
@ -58,13 +58,18 @@ pub const EXERCISE_FIELDS: [&str; 13] = ["schema", "id", "translations", "title"
|
|
|
|
|
/// The fields an exercise must have.
|
|
|
|
|
pub const EXERCISE_REQUIRED: [&str; 8] = ["schema", "id", "title", "task", "starter", "hints", "reference", "wrong"];
|
|
|
|
|
/// The constraints an exercise may set.
|
|
|
|
|
pub const CONSTRAINTS: [&str; 6] = ["allow", "counts", "t_count", "two_qubit", "no_smells", "estimate"];
|
|
|
|
|
pub const CONSTRAINTS: [&str; 8] = ["allow", "counts", "t_count", "two_qubit", "no_smells", "estimate", "qubits", "inject"];
|
|
|
|
|
/// What an exercise's job may give.
|
|
|
|
|
pub const JOB_FIELDS: [&str; 3] = ["shots", "seed", "noise"];
|
|
|
|
|
/// The resource estimate's counts a constraint may bound.
|
|
|
|
|
pub const ESTIMATE_COUNTS: [&str; 5] = ["toffolis", "t_gates", "rotations", "logical_qubits", "t_states"];
|
|
|
|
|
/// Why a known wrong answer fails.
|
|
|
|
|
pub const FAILS: [&str; 3] = ["intent", "constraint", "refused"];
|
|
|
|
|
/// The most cases an exercise may have: each is a run of the program, and its
|
|
|
|
|
/// verdict, in every check.
|
|
|
|
|
pub const MOST_CASES: usize = 16;
|
|
|
|
|
/// The most intents an exercise, or one of its cases, may give.
|
|
|
|
|
pub const MOST_INTENTS: usize = 8;
|
|
|
|
|
|
|
|
|
|
const COURSE: &str = include_str!("../../course/course.json");
|
|
|
|
|
const MESSAGES: [(&str, &str); 2] = [("en", include_str!("../../course/locales/en.json")), ("zh-CN", include_str!("../../course/locales/zh-CN.json"))];
|
|
|
|
|
@ -179,6 +184,185 @@ fn bound(v: &Value, what: &str) -> Result<(), String> {
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// A bound the course says as "at most n" (a T count, a two-qubit count, an
|
|
|
|
|
/// estimate's count): `{"max": n}` alone, so what the check holds a program to
|
|
|
|
|
/// is what its words say.
|
|
|
|
|
fn at_most(v: &Value, what: &str) -> Result<(), String> {
|
|
|
|
|
bound(v, what)?;
|
|
|
|
|
if v.as_object().is_none_or(|o| o.len() != 1 || !o.contains_key("max")) {
|
|
|
|
|
return Err(format!("{what}: {{\"max\": n}} alone: the course says this bound as at most n"));
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// A field of an exercise or a case, null read as absent.
|
|
|
|
|
fn present<'a>(o: &'a Map<String, Value>, k: &str) -> Option<&'a Value> {
|
|
|
|
|
o.get(k).filter(|v| !v.is_null())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// An intent (one, or a list) checked in its shape and its number.
|
|
|
|
|
fn intent_list(v: &Value, what: &str) -> Result<(), String> {
|
|
|
|
|
let list = verdict::intents(v).map_err(|e| format!("{what}: {e}"))?;
|
|
|
|
|
if list.len() > MOST_INTENTS {
|
|
|
|
|
return Err(format!("{what}: {} intents, and one may give at most {MOST_INTENTS}", list.len()));
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// What an exercise asks of a program, checked in its form as a check reads
|
|
|
|
|
/// it: its job (shots, seed and noise, nothing else), its intent, its cases
|
|
|
|
|
/// (at most [`MOST_CASES`], each with its inputs as whole numbers and an
|
|
|
|
|
/// intent of its own or the exercise's) and its constraints (each one the
|
|
|
|
|
/// course names, each bound whole numbers). A field that is null is absent.
|
|
|
|
|
/// Nothing malformed is read as no bound, or dropped: a check of an exercise
|
|
|
|
|
/// that does not read here would hold vacuously where it reads nothing.
|
|
|
|
|
fn asks(o: &Map<String, Value>, id: &str) -> Result<(), String> {
|
|
|
|
|
if let Some(job) = present(o, "job") {
|
|
|
|
|
let j = job.as_object().ok_or_else(|| format!("{id}: job is an object"))?;
|
|
|
|
|
if let Some(k) = j.keys().find(|k| !JOB_FIELDS.contains(&k.as_str())) {
|
|
|
|
|
return Err(format!("{id}: job takes {}, not {k}", JOB_FIELDS.join(", ")));
|
|
|
|
|
}
|
|
|
|
|
if present(j, "shots").is_some_and(|s| !s.as_u64().is_some_and(|n| n >= 1)) {
|
|
|
|
|
return Err(format!("{id}: job: shots is a whole number, at least 1"));
|
|
|
|
|
}
|
|
|
|
|
if present(j, "seed").is_some_and(|s| s.as_u64().is_none()) {
|
|
|
|
|
return Err(format!("{id}: job: seed is a whole number"));
|
|
|
|
|
}
|
|
|
|
|
if let Some(n) = present(j, "noise") {
|
|
|
|
|
let n = n.as_object().ok_or_else(|| format!("{id}: job: noise is an object of channels and their probabilities"))?;
|
|
|
|
|
if let Some((k, _)) = n.iter().find(|(_, p)| !p.as_f64().is_some_and(|p| (0.0..=1.0).contains(&p))) {
|
|
|
|
|
return Err(format!("{id}: job: noise: {k} takes a probability, 0 to 1"));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
let intent = present(o, "intent");
|
|
|
|
|
let cases = present(o, "cases");
|
|
|
|
|
if intent.is_none() && cases.is_none() {
|
|
|
|
|
return Err(format!("{id}: an exercise needs an intent, or cases that each carry one"));
|
|
|
|
|
}
|
|
|
|
|
if let Some(i) = intent {
|
|
|
|
|
intent_list(i, &format!("{id}: intent"))?;
|
|
|
|
|
}
|
|
|
|
|
if let Some(cases) = cases {
|
|
|
|
|
let list = cases.as_array().filter(|a| !a.is_empty()).ok_or_else(|| format!("{id}: cases is a list of at least one case"))?;
|
|
|
|
|
if list.len() > MOST_CASES {
|
|
|
|
|
return Err(format!("{id}: {} cases, and an exercise may have at most {MOST_CASES}", list.len()));
|
|
|
|
|
}
|
|
|
|
|
for (n, c) in list.iter().enumerate() {
|
|
|
|
|
let what = format!("{id}: case {}", n + 1);
|
|
|
|
|
let co = c.as_object().ok_or_else(|| format!("{what} is an object"))?;
|
|
|
|
|
if let Some(k) = co.keys().find(|k| !["inputs", "name", "intent"].contains(&k.as_str())) {
|
|
|
|
|
return Err(format!("{what}: `{k}` is not a field of a case (inputs, name, intent)"));
|
|
|
|
|
}
|
|
|
|
|
if let Some(ins) = present(co, "inputs") {
|
|
|
|
|
let ins = ins.as_object().ok_or_else(|| format!("{what}: inputs is an object of whole numbers"))?;
|
|
|
|
|
if ins.values().any(|x| x.as_i64().is_none()) {
|
|
|
|
|
return Err(format!("{what}: inputs is an object of whole numbers"));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if let Some(nm) = present(co, "name")
|
|
|
|
|
&& !(nm.is_string() || nm.as_object().is_some_and(|m| m.values().all(Value::is_string)))
|
|
|
|
|
{
|
|
|
|
|
return Err(format!("{what}: name is a text, or one for each locale"));
|
|
|
|
|
}
|
|
|
|
|
match present(co, "intent") {
|
|
|
|
|
Some(i) => intent_list(i, &format!("{what}: intent"))?,
|
|
|
|
|
None if intent.is_none() => return Err(format!("{what}: no intent, and the exercise has none")),
|
|
|
|
|
None => {}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if let Some(c) = present(o, "constraints") {
|
|
|
|
|
let co = c.as_object().ok_or_else(|| format!("{id}: constraints is an object"))?;
|
|
|
|
|
if let Some(k) = co.keys().find(|k| !CONSTRAINTS.contains(&k.as_str())) {
|
|
|
|
|
return Err(format!("{id}: `{k}` is not a constraint ({})", CONSTRAINTS.join(", ")));
|
|
|
|
|
}
|
|
|
|
|
if let Some(a) = co.get("allow") {
|
|
|
|
|
let gates = a.as_array().filter(|a| !a.is_empty() && a.iter().all(Value::is_string)).ok_or_else(|| format!("{id}: allow is a list of gate names"))?;
|
|
|
|
|
if let Some(g) = gates.iter().filter_map(Value::as_str).find(|g| !GATES.contains(g)) {
|
|
|
|
|
return Err(format!("{id}: allow: {g} is not a gate the reader names"));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if let Some(cs) = co.get("counts") {
|
|
|
|
|
for (g, b) in cs.as_object().ok_or_else(|| format!("{id}: counts is an object of gates and bounds"))? {
|
|
|
|
|
if !GATES.contains(&g.as_str()) {
|
|
|
|
|
return Err(format!("{id}: counts: {g} is not a gate the reader names"));
|
|
|
|
|
}
|
|
|
|
|
bound(b, &format!("{id}: counts: {g}"))?;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for k in ["t_count", "two_qubit"] {
|
|
|
|
|
if let Some(b) = co.get(k) {
|
|
|
|
|
at_most(b, &format!("{id}: {k}"))?;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if let Some(s) = co.get("no_smells") {
|
|
|
|
|
let kinds = s.as_array().filter(|a| !a.is_empty()).ok_or_else(|| format!("{id}: no_smells is a list of the critique's smell kinds"))?;
|
|
|
|
|
if let Some(k) = kinds.iter().find(|k| !k.as_str().is_some_and(|k| SMELLS.contains(&k))) {
|
|
|
|
|
return Err(format!("{id}: no_smells: {k} is not a smell the critique names ({})", SMELLS.join(", ")));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if let Some(e) = co.get("estimate") {
|
|
|
|
|
for (k, b) in e.as_object().ok_or_else(|| format!("{id}: estimate is an object of the estimate's counts and bounds"))? {
|
|
|
|
|
if !ESTIMATE_COUNTS.contains(&k.as_str()) {
|
|
|
|
|
return Err(format!("{id}: estimate: {k} is not a count the estimate gives ({})", ESTIMATE_COUNTS.join(", ")));
|
|
|
|
|
}
|
|
|
|
|
at_most(b, &format!("{id}: estimate: {k}"))?;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if let Some(b) = co.get("qubits") {
|
|
|
|
|
at_most(b, &format!("{id}: qubits"))?;
|
|
|
|
|
}
|
|
|
|
|
if let Some(i) = co.get("inject") {
|
|
|
|
|
let name = i.as_str().filter(|n| !n.is_empty()).ok_or_else(|| format!("{id}: inject names the input whose value is the qubit an error flips, as \"err\""))?;
|
|
|
|
|
let values: Vec<Option<i64>> = cases.and_then(Value::as_array).into_iter().flatten().map(|c| c.pointer(&format!("/inputs/{name}")).and_then(Value::as_i64)).collect();
|
|
|
|
|
if values.iter().any(|v| v.is_none_or(|v| v < 0)) || values.iter().collect::<std::collections::BTreeSet<_>>().len() < 2 {
|
|
|
|
|
return Err(format!("{id}: inject: every case gives {name}, a qubit (a whole number from 0), and at least two cases give different ones"));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Whether `exercise` is in the form [`EXERCISE_SCHEMA`] names, as far as a
|
|
|
|
|
/// check reads it: a JSON object of an exercise's fields only, naming that
|
|
|
|
|
/// form if it names one, with its texts and programs where it gives them, and
|
|
|
|
|
/// what it asks of a program (its job, intent, cases and constraints) in the
|
|
|
|
|
/// form a lesson's exercise is held to. An exercise that is no lesson's (a
|
|
|
|
|
/// task a program is written for) need not give a lesson's title in every
|
|
|
|
|
/// locale, its hints, solution or known wrong answers, nor name each case.
|
|
|
|
|
/// [`check_exercise`] refuses what this refuses, before it runs anything.
|
|
|
|
|
pub fn exercise_form(exercise: &Value) -> Result<(), String> {
|
|
|
|
|
let id = "the exercise";
|
|
|
|
|
let o = exercise.as_object().ok_or_else(|| format!("{id} is not a JSON object"))?;
|
|
|
|
|
if let Some(k) = o.keys().find(|k| !EXERCISE_FIELDS.contains(&k.as_str())) {
|
|
|
|
|
return Err(format!("{id}: `{k}` is not a field of an exercise ({})", EXERCISE_FIELDS.join(", ")));
|
|
|
|
|
}
|
|
|
|
|
if let Some(s) = present(o, "schema")
|
|
|
|
|
&& *s != EXERCISE_SCHEMA
|
|
|
|
|
{
|
|
|
|
|
return Err(format!("{id}: its schema is {s}, not \"{EXERCISE_SCHEMA}\""));
|
|
|
|
|
}
|
|
|
|
|
for k in ["title", "task"] {
|
|
|
|
|
if let Some(t) = present(o, k)
|
|
|
|
|
&& !(t.is_string() || t.as_object().is_some_and(|m| m.values().all(Value::is_string)))
|
|
|
|
|
{
|
|
|
|
|
return Err(format!("{id}: {k} is a text, or one for each locale"));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for k in ["starter", "reference"] {
|
|
|
|
|
if let Some(p) = present(o, k) {
|
|
|
|
|
program_text(p, &format!("{id}: {k}"))?;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for (k, ok) in [("id", Value::is_string as fn(&Value) -> bool), ("translations", Value::is_object), ("hints", Value::is_object), ("wrong", Value::is_array)] {
|
|
|
|
|
if present(o, k).is_some_and(|v| !ok(v)) {
|
|
|
|
|
return Err(format!("{id}: {k} is not in an exercise's form"));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
asks(o, id)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// An exercise read from its JSON and checked in its form, its programs
|
|
|
|
|
/// joined from their lines.
|
|
|
|
|
fn read_exercise(id: &str, text: &str) -> Result<Value, String> {
|
|
|
|
|
@ -200,76 +384,14 @@ fn read_exercise(id: &str, text: &str) -> Result<Value, String> {
|
|
|
|
|
if counts[0] == 0 || counts.iter().any(|c| *c != counts[0]) || hints.len() != LOCALES.len() {
|
|
|
|
|
return Err(format!("{id}: hints needs the same number of hints, at least one, in each locale ({counts:?})"));
|
|
|
|
|
}
|
|
|
|
|
if let Some(job) = o.get("job") {
|
|
|
|
|
let j = job.as_object().ok_or_else(|| format!("{id}: job is an object"))?;
|
|
|
|
|
if let Some(k) = j.keys().find(|k| !JOB_FIELDS.contains(&k.as_str())) {
|
|
|
|
|
return Err(format!("{id}: job takes {}, not {k}", JOB_FIELDS.join(", ")));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if !o.contains_key("intent") && !o.contains_key("cases") {
|
|
|
|
|
return Err(format!("{id}: an exercise needs an intent, or cases that each carry one"));
|
|
|
|
|
}
|
|
|
|
|
if let Some(i) = o.get("intent") {
|
|
|
|
|
verdict::intents(i).map_err(|e| format!("{id}: intent: {e}"))?;
|
|
|
|
|
}
|
|
|
|
|
if let Some(cases) = o.get("cases") {
|
|
|
|
|
let list = cases.as_array().filter(|a| !a.is_empty()).ok_or_else(|| format!("{id}: cases is a list of at least one case"))?;
|
|
|
|
|
for (n, c) in list.iter().enumerate() {
|
|
|
|
|
let what = format!("{id}: case {}", n + 1);
|
|
|
|
|
let co = c.as_object().ok_or_else(|| format!("{what} is an object"))?;
|
|
|
|
|
if let Some(k) = co.keys().find(|k| !["inputs", "name", "intent"].contains(&k.as_str())) {
|
|
|
|
|
return Err(format!("{what}: `{k}` is not a field of a case (inputs, name, intent)"));
|
|
|
|
|
}
|
|
|
|
|
let ins = co.get("inputs").and_then(Value::as_object).ok_or_else(|| format!("{what}: inputs is an object of whole numbers"))?;
|
|
|
|
|
if ins.values().any(|x| x.as_i64().is_none()) {
|
|
|
|
|
return Err(format!("{what}: inputs is an object of whole numbers"));
|
|
|
|
|
}
|
|
|
|
|
localized(co.get("name"), &format!("{what}: name"))?;
|
|
|
|
|
match co.get("intent") {
|
|
|
|
|
Some(i) => drop(verdict::intents(i).map_err(|e| format!("{what}: intent: {e}"))?),
|
|
|
|
|
None if !o.contains_key("intent") => return Err(format!("{what}: no intent, and the exercise has none")),
|
|
|
|
|
None => {}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if let Some(c) = o.get("constraints") {
|
|
|
|
|
let co = c.as_object().ok_or_else(|| format!("{id}: constraints is an object"))?;
|
|
|
|
|
if let Some(k) = co.keys().find(|k| !CONSTRAINTS.contains(&k.as_str())) {
|
|
|
|
|
return Err(format!("{id}: `{k}` is not a constraint ({})", CONSTRAINTS.join(", ")));
|
|
|
|
|
}
|
|
|
|
|
if let Some(a) = co.get("allow") {
|
|
|
|
|
let gates = a.as_array().filter(|a| !a.is_empty() && a.iter().all(Value::is_string)).ok_or_else(|| format!("{id}: allow is a list of gate names"))?;
|
|
|
|
|
if let Some(g) = gates.iter().filter_map(Value::as_str).find(|g| !GATES.contains(g)) {
|
|
|
|
|
return Err(format!("{id}: allow: {g} is not a gate the reader names"));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if let Some(cs) = co.get("counts") {
|
|
|
|
|
for (g, b) in cs.as_object().ok_or_else(|| format!("{id}: counts is an object of gates and bounds"))? {
|
|
|
|
|
if !GATES.contains(&g.as_str()) {
|
|
|
|
|
return Err(format!("{id}: counts: {g} is not a gate the reader names"));
|
|
|
|
|
}
|
|
|
|
|
bound(b, &format!("{id}: counts: {g}"))?;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for k in ["t_count", "two_qubit"] {
|
|
|
|
|
if let Some(b) = co.get(k) {
|
|
|
|
|
bound(b, &format!("{id}: {k}"))?;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if let Some(s) = co.get("no_smells") {
|
|
|
|
|
let kinds = s.as_array().filter(|a| !a.is_empty()).ok_or_else(|| format!("{id}: no_smells is a list of the critique's smell kinds"))?;
|
|
|
|
|
if let Some(k) = kinds.iter().find(|k| !k.as_str().is_some_and(|k| SMELLS.contains(&k))) {
|
|
|
|
|
return Err(format!("{id}: no_smells: {k} is not a smell the critique names ({})", SMELLS.join(", ")));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if let Some(e) = co.get("estimate") {
|
|
|
|
|
for (k, b) in e.as_object().ok_or_else(|| format!("{id}: estimate is an object of the estimate's counts and bounds"))? {
|
|
|
|
|
if !ESTIMATE_COUNTS.contains(&k.as_str()) {
|
|
|
|
|
return Err(format!("{id}: estimate: {k} is not a count the estimate gives ({})", ESTIMATE_COUNTS.join(", ")));
|
|
|
|
|
}
|
|
|
|
|
bound(b, &format!("{id}: estimate: {k}"))?;
|
|
|
|
|
}
|
|
|
|
|
asks(o, id)?;
|
|
|
|
|
// A lesson's cases each name their inputs, and are named in every locale.
|
|
|
|
|
for (n, c) in present(o, "cases").and_then(Value::as_array).into_iter().flatten().enumerate() {
|
|
|
|
|
let what = format!("{id}: case {}", n + 1);
|
|
|
|
|
if !c.get("inputs").is_some_and(Value::is_object) {
|
|
|
|
|
return Err(format!("{what}: inputs is an object of whole numbers"));
|
|
|
|
|
}
|
|
|
|
|
localized(c.get("name"), &format!("{what}: name"))?;
|
|
|
|
|
}
|
|
|
|
|
let wrong = o["wrong"].as_array().filter(|w| !w.is_empty()).ok_or_else(|| format!("{id}: wrong is a list of at least one known wrong answer"))?;
|
|
|
|
|
for (n, w) in wrong.iter().enumerate() {
|
|
|
|
|
@ -367,6 +489,12 @@ fn word<'a>(v: &'a Value, loc: &str) -> &'a str {
|
|
|
|
|
v.get(loc).or_else(|| v.get(LOCALES[0])).and_then(Value::as_str).unwrap_or("")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// A text given once, or in each locale ([`word`]): an exercise that is no
|
|
|
|
|
/// lesson's may name a case with one string.
|
|
|
|
|
fn text_in<'a>(v: &'a Value, loc: &str) -> &'a str {
|
|
|
|
|
v.as_str().unwrap_or_else(|| word(v, loc))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// A probability or a distance, as the course writes one: up to four
|
|
|
|
|
/// decimals, without trailing zeros, or in scientific notation when small.
|
|
|
|
|
fn num(x: f64) -> String {
|
|
|
|
|
@ -451,18 +579,57 @@ fn constraint_texts(c: &Value, loc: &str) -> Vec<(String, String, String)> {
|
|
|
|
|
for (k, b) in o.get("estimate").and_then(Value::as_object).into_iter().flatten() {
|
|
|
|
|
out.push(("estimate".into(), k.clone(), say(loc, "constraint.estimate", &[("n", b["max"].as_u64().map_or("?".into(), |n| n.to_string())), ("what", say(loc, &format!("estimate.{k}"), &[]))])));
|
|
|
|
|
}
|
|
|
|
|
if let Some(b) = o.get("qubits") {
|
|
|
|
|
out.push(("qubits".into(), String::new(), say(loc, "constraint.qubits", &[("n", b["max"].as_u64().map_or("?".into(), |n| n.to_string()))])));
|
|
|
|
|
}
|
|
|
|
|
if let Some(i) = o.get("inject").and_then(Value::as_str) {
|
|
|
|
|
out.push(("inject".into(), i.to_string(), say(loc, "constraint.inject", &[("input", i.to_string())])));
|
|
|
|
|
}
|
|
|
|
|
out
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Whether the programs of a task's cases (each with the value its case gives the input an
|
|
|
|
|
/// error is injected by) differ only where the error is injected: one gate, an x on the qubit
|
|
|
|
|
/// the case's value names, every other gate, measurement and step of every case's program the
|
|
|
|
|
/// same. Then the error is in the quantum state and nothing else in the program reads it, so
|
|
|
|
|
/// what corrects or detects it is the program's quantum operations, the same in every case.
|
|
|
|
|
/// Otherwise, the locale key of why not.
|
|
|
|
|
fn injected(programs: &[(&qasm::Program, i64)]) -> Result<(), &'static str> {
|
|
|
|
|
let Some((first, _)) = programs.first() else { return Ok(()) };
|
|
|
|
|
if programs.iter().any(|(p, _)| p.ops.len() != first.ops.len()) {
|
|
|
|
|
return Err("wrong.inject_read");
|
|
|
|
|
}
|
|
|
|
|
let differ: Vec<usize> = (0..first.ops.len()).filter(|&j| programs.iter().any(|(p, _)| p.ops[j] != first.ops[j])).collect();
|
|
|
|
|
let at = match differ[..] {
|
|
|
|
|
[] => return Err("wrong.inject_unused"),
|
|
|
|
|
[at] => at,
|
|
|
|
|
_ => return Err("wrong.inject_read"),
|
|
|
|
|
};
|
|
|
|
|
if programs.iter().any(|(p, v)| p.ops[at].kind != qasm::Kind::X || u32::try_from(*v).ok().is_none_or(|q| p.ops[at].qubits != [q])) {
|
|
|
|
|
return Err("wrong.inject_where");
|
|
|
|
|
}
|
|
|
|
|
let rest = |p: &qasm::Program| {
|
|
|
|
|
let mut ops = p.ops.clone();
|
|
|
|
|
ops[at] = first.ops[at].clone();
|
|
|
|
|
(p.n_qubits, p.n_bits, ops, p.measured.clone(), p.dynamic.clone())
|
|
|
|
|
};
|
|
|
|
|
if programs.iter().any(|(p, _)| rest(p) != rest(first)) {
|
|
|
|
|
return Err("wrong.inject_read");
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The cases a program is checked in: each with its name, the inputs it
|
|
|
|
|
/// gives a generator, and its intent.
|
|
|
|
|
fn cases_of(ex: &Value) -> Vec<(Option<&Value>, qasm::Inputs, Value)> {
|
|
|
|
|
fn cases_of<'a>(ex: &'a Value) -> Vec<(Option<&'a Value>, qasm::Inputs, Value)> {
|
|
|
|
|
match ex.get("cases").and_then(Value::as_array) {
|
|
|
|
|
Some(list) => list
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|c| {
|
|
|
|
|
let ins: qasm::Inputs = c["inputs"].as_object().into_iter().flatten().map(|(k, v)| (k.clone(), v.as_i64().unwrap_or(0))).collect();
|
|
|
|
|
(Some(&c["name"]), ins, c.get("intent").or(ex.get("intent")).cloned().unwrap_or(Value::Null))
|
|
|
|
|
// A case's intent, or the exercise's when it gives none (null is none).
|
|
|
|
|
let own = |v: &'a Value| v.get("intent").filter(|i| !i.is_null());
|
|
|
|
|
(Some(&c["name"]), ins, own(c).or(own(ex)).cloned().unwrap_or(Value::Null))
|
|
|
|
|
})
|
|
|
|
|
.collect(),
|
|
|
|
|
None => vec![(None, qasm::Inputs::new(), ex["intent"].clone())],
|
|
|
|
|
@ -694,23 +861,83 @@ fn limit_text(b: &Value, loc: &str) -> String {
|
|
|
|
|
/// proof or as evidence, and every constraint holds.
|
|
|
|
|
pub fn check(id: &str, src: &str, job_bytes: u64, loc: &str) -> Result<Value, String> {
|
|
|
|
|
let (c, i) = find(id)?;
|
|
|
|
|
check_exercise_as(&c.lessons[i].exercise, Some(id), src, job_bytes, loc)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// A program checked against any exercise in the form [`EXERCISE_SCHEMA`]
|
|
|
|
|
/// names, as [`check`] checks a lesson's: the same verdicts, constraints and
|
|
|
|
|
/// words, with `lesson` null. It is how a task that is not in the course (a
|
|
|
|
|
/// program a person or the job service is asked to write) is held to what it
|
|
|
|
|
/// must do.
|
|
|
|
|
pub fn check_exercise(exercise: &Value, src: &str, job_bytes: u64, loc: &str) -> Result<Value, String> {
|
|
|
|
|
exercise_form(exercise)?;
|
|
|
|
|
check_exercise_as(exercise, None, src, job_bytes, loc)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The T gates a program comes to, as a T count bounds it: a T or T† one each,
|
|
|
|
|
/// a Clifford gate none, and a phase rotation (`p`, `rz`) by an odd multiple of
|
|
|
|
|
/// π/4 one, since it is a T or T† up to Clifford gates and a global phase. Any
|
|
|
|
|
/// other gate's T count is not fixed (it depends on how the gate is written in
|
|
|
|
|
/// Clifford and T gates, or it has no exact form in them): that gate's name.
|
|
|
|
|
fn t_gates(p: &qasm::Program) -> Result<u64, &'static str> {
|
|
|
|
|
let mut n = 0;
|
|
|
|
|
for op in &p.ops {
|
|
|
|
|
let odd_quarter = |theta: f64| {
|
|
|
|
|
let q = theta / std::f64::consts::FRAC_PI_4;
|
|
|
|
|
(q - q.round()).abs() < 1e-9 && (q.round() as i64).rem_euclid(2) == 1
|
|
|
|
|
};
|
|
|
|
|
if qasm::Program::lower_clifford(std::slice::from_ref(op)).is_ok() {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
match op.kind {
|
|
|
|
|
qasm::Kind::T | qasm::Kind::Tdg => n += 1,
|
|
|
|
|
qasm::Kind::P | qasm::Kind::Rz if op.params.first().is_some_and(|t| odd_quarter(*t)) => n += 1,
|
|
|
|
|
k => return Err(critique::kind_name(k)),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Ok(n)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The two-qubit gates of a program, as a two-qubit count bounds it; a gate on
|
|
|
|
|
/// three qubits is not one, and is not bounded by their count: its name.
|
|
|
|
|
fn two_qubit_gates(p: &qasm::Program) -> Result<u64, &'static str> {
|
|
|
|
|
if let Some(op) = p.ops.iter().find(|o| o.qubits.len() > 2) {
|
|
|
|
|
return Err(critique::kind_name(op.kind));
|
|
|
|
|
}
|
|
|
|
|
Ok(p.ops.iter().filter(|o| o.qubits.len() == 2).count() as u64)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// One case's program as the constraints measure it: the case's number and
|
|
|
|
|
/// name where the cases are named, its text, the critique of it, and it read.
|
|
|
|
|
struct Measured<'a> {
|
|
|
|
|
case: Option<usize>,
|
|
|
|
|
name: Option<String>,
|
|
|
|
|
inputs: qasm::Inputs,
|
|
|
|
|
text: &'a str,
|
|
|
|
|
critique: Value,
|
|
|
|
|
program: Option<qasm::Program>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn check_exercise_as(ex: &Value, id: Option<&str>, src: &str, job_bytes: u64, loc: &str) -> Result<Value, String> {
|
|
|
|
|
let loc = locale(loc);
|
|
|
|
|
let ex = &c.lessons[i].exercise;
|
|
|
|
|
let cases = cases_of(ex);
|
|
|
|
|
let named = cases.len() > 1 || ex.get("cases").is_some();
|
|
|
|
|
let declared = qasm::inputs(src).unwrap_or_default();
|
|
|
|
|
let named = cases.len() > 1 || ex.get("cases").is_some_and(Value::is_array);
|
|
|
|
|
let mut wrongs: Vec<Wrong> = Vec::new();
|
|
|
|
|
let mut case_out = Vec::new();
|
|
|
|
|
let mut first_text: Option<String> = None;
|
|
|
|
|
// Each case's program as it reads there (a generator at the case's inputs),
|
|
|
|
|
// with the case's number and name: what the constraints are held to.
|
|
|
|
|
let mut texts: Vec<(Option<usize>, Option<String>, String, qasm::Inputs)> = Vec::new();
|
|
|
|
|
let mut decided = true;
|
|
|
|
|
let mut levels: Vec<verdict::Level> = Vec::new();
|
|
|
|
|
for (n, (name, inputs, intent)) in cases.iter().enumerate() {
|
|
|
|
|
let ins: qasm::Inputs = inputs.iter().filter(|(k, _)| declared.contains(k)).map(|(k, v)| (k.clone(), *v)).collect();
|
|
|
|
|
let case = named.then_some(n);
|
|
|
|
|
let v = match qasm::read_with(src, &ins) {
|
|
|
|
|
// Every input a case gives binds: the reader refuses a value for an input
|
|
|
|
|
// the program does not declare (Q114), so a program that ignores the
|
|
|
|
|
// perturbation a case is about does not pass that case.
|
|
|
|
|
let v = match qasm::read_with(src, inputs) {
|
|
|
|
|
Err(d) => verdict::of_diagnostic(&d),
|
|
|
|
|
Ok(_) => {
|
|
|
|
|
let text = if ins.is_empty() { Ok(src.to_string()) } else { qasm::instantiate(src, &ins) };
|
|
|
|
|
let text = if inputs.is_empty() { Ok(src.to_string()) } else { qasm::instantiate(src, inputs) };
|
|
|
|
|
let text = match text {
|
|
|
|
|
Ok(t) => t,
|
|
|
|
|
Err(e) => {
|
|
|
|
|
@ -718,14 +945,12 @@ pub fn check(id: &str, src: &str, job_bytes: u64, loc: &str) -> Result<Value, St
|
|
|
|
|
levels.push(verdict::Level::Refused);
|
|
|
|
|
decided = false;
|
|
|
|
|
wrongs.extend(verdict_wrongs(&v, case, loc));
|
|
|
|
|
case_out.push(json!({ "name": name.map(|x| word(x, loc)), "inputs": inputs, "passed": false, "level": v["level"], "holds": v["holds"], "statement": v["statement"], "verdict": v }));
|
|
|
|
|
case_out.push(json!({ "name": name.map(|x| text_in(x, loc)), "inputs": inputs, "passed": false, "level": v["level"], "holds": v["holds"], "statement": v["statement"], "verdict": v }));
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
if first_text.is_none() {
|
|
|
|
|
first_text = Some(text.clone());
|
|
|
|
|
}
|
|
|
|
|
let mut req = ex.get("job").cloned().unwrap_or(json!({}));
|
|
|
|
|
texts.push((case, name.map(|x| text_in(x, loc).to_string()), text.clone(), inputs.clone()));
|
|
|
|
|
let mut req = ex.get("job").filter(|j| j.is_object()).cloned().unwrap_or(json!({}));
|
|
|
|
|
req["qasm"] = json!(text);
|
|
|
|
|
req["intent"] = intent.clone();
|
|
|
|
|
if req.get("shots").is_none() {
|
|
|
|
|
@ -746,7 +971,7 @@ pub fn check(id: &str, src: &str, job_bytes: u64, loc: &str) -> Result<Value, St
|
|
|
|
|
if v["holds"].is_null() {
|
|
|
|
|
decided = false;
|
|
|
|
|
}
|
|
|
|
|
let name_text = name.map(|x| word(x, loc).to_string());
|
|
|
|
|
let name_text = name.map(|x| text_in(x, loc).to_string());
|
|
|
|
|
for mut w in verdict_wrongs(&v, case, loc) {
|
|
|
|
|
if let Some(nm) = &name_text {
|
|
|
|
|
w.text = say(loc, "wrong.case", &[("name", nm.clone()), ("text", w.text)]);
|
|
|
|
|
@ -763,18 +988,42 @@ pub fn check(id: &str, src: &str, job_bytes: u64, loc: &str) -> Result<Value, St
|
|
|
|
|
"verdict": v,
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
// The constraints, on the program as it reads (a generator at its first case).
|
|
|
|
|
// The constraints, held on the program as it reads in every case: a gate
|
|
|
|
|
// that one case alone uses is held to them as the first case's are. A
|
|
|
|
|
// constraint holds when it holds in every case; what it found is the first
|
|
|
|
|
// case's, or the first case's where it does not hold.
|
|
|
|
|
let first_text = texts.first().map(|(_, _, t, _)| t.clone());
|
|
|
|
|
let mut constraints = Vec::new();
|
|
|
|
|
let mut measures = Value::Null;
|
|
|
|
|
let mut estimate_out = Value::Null;
|
|
|
|
|
let cons = ex.get("constraints").cloned().unwrap_or(json!({}));
|
|
|
|
|
if let Some(text) = &first_text {
|
|
|
|
|
let m = critique::critique(text);
|
|
|
|
|
if m["ok"] == true {
|
|
|
|
|
measures = json!({ "qubits": m["qubits"], "gates": m["gates"], "two_qubit": m["two_qubit"], "t_count": m["t_count"], "depth": m["depth"], "by_gate": m["by_gate"] });
|
|
|
|
|
let by = m["by_gate"].as_object().cloned().unwrap_or_default();
|
|
|
|
|
let count = |g: &str| by.get(g).and_then(Value::as_u64).unwrap_or(0);
|
|
|
|
|
for (rule, subject, text_c) in constraint_texts(&cons, loc) {
|
|
|
|
|
let cons = ex.get("constraints").filter(|c| c.is_object()).cloned().unwrap_or(json!({}));
|
|
|
|
|
let measured: Vec<Measured> = texts.iter().map(|(c, nm, t, ins)| Measured { case: *c, name: nm.clone(), inputs: ins.clone(), text: t, critique: critique::critique(t), program: qasm::parse(t).ok() }).collect();
|
|
|
|
|
if let Some(m0) = measured.first().map(|x| &x.critique)
|
|
|
|
|
&& m0["ok"] == true
|
|
|
|
|
{
|
|
|
|
|
measures = json!({ "qubits": m0["qubits"], "gates": m0["gates"], "two_qubit": m0["two_qubit"], "t_count": m0["t_count"], "depth": m0["depth"], "by_gate": m0["by_gate"] });
|
|
|
|
|
let mut estimates: Vec<Value> = Vec::new();
|
|
|
|
|
for (rule, subject, text_c) in constraint_texts(&cons, loc) {
|
|
|
|
|
// An injected error is held across the cases' programs at once.
|
|
|
|
|
if rule == "inject" {
|
|
|
|
|
let programs: Vec<(&qasm::Program, i64)> = measured.iter().filter_map(|x| Some((x.program.as_ref()?, *x.inputs.get(&subject)?))).collect();
|
|
|
|
|
if programs.len() < 2 {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
let holds = injected(&programs);
|
|
|
|
|
if let Err(why) = holds {
|
|
|
|
|
wrongs.push(Wrong { kind: "constraint", text: say(loc, why, &[("input", subject.clone())]), engine: None, lines: Vec::new(), case: None, pointer: say(loc, "pointer.inject", &[("input", subject.clone())]) });
|
|
|
|
|
}
|
|
|
|
|
constraints.push(json!({ "rule": rule, "subject": subject, "text": text_c, "holds": holds.is_ok(), "found": holds.err().map(|w| w.trim_start_matches("wrong.inject_")) }));
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
let mut row: Option<(bool, Value, Option<Wrong>)> = None;
|
|
|
|
|
for (k, Measured { case, name: case_name, critique: m, program, .. }) in measured.iter().enumerate() {
|
|
|
|
|
if m["ok"] != true {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
let by = m["by_gate"].as_object().cloned().unwrap_or_default();
|
|
|
|
|
let count = |g: &str| by.get(g).and_then(Value::as_u64).unwrap_or(0);
|
|
|
|
|
let (holds, found, wrong) = match rule.as_str() {
|
|
|
|
|
"allow" => {
|
|
|
|
|
let allowed: Vec<&str> = cons["allow"].as_array().into_iter().flatten().filter_map(Value::as_str).collect();
|
|
|
|
|
@ -804,14 +1053,49 @@ pub fn check(id: &str, src: &str, job_bytes: u64, loc: &str) -> Result<Value, St
|
|
|
|
|
}
|
|
|
|
|
"t_count" | "two_qubit" => {
|
|
|
|
|
let b = &cons[&rule];
|
|
|
|
|
let n = m[&rule].as_u64().unwrap_or(0);
|
|
|
|
|
let limit = b["max"].as_u64().map_or("?".into(), |x| x.to_string());
|
|
|
|
|
let counted = match program {
|
|
|
|
|
Some(p) if rule == "t_count" => t_gates(p),
|
|
|
|
|
Some(p) => two_qubit_gates(p),
|
|
|
|
|
None => Ok(m[&rule].as_u64().unwrap_or(0)),
|
|
|
|
|
};
|
|
|
|
|
match counted {
|
|
|
|
|
Ok(n) => {
|
|
|
|
|
let w = (!within(n, b)).then(|| Wrong {
|
|
|
|
|
kind: "constraint",
|
|
|
|
|
text: say(loc, &format!("wrong.{rule}"), &[("found", n.to_string()), ("limit", limit.clone())]),
|
|
|
|
|
engine: None,
|
|
|
|
|
lines: Vec::new(),
|
|
|
|
|
case: None,
|
|
|
|
|
pointer: say(loc, &format!("pointer.{rule}"), &[]),
|
|
|
|
|
});
|
|
|
|
|
(within(n, b), json!(n), w)
|
|
|
|
|
}
|
|
|
|
|
// A gate the count cannot bound: the constraint does not hold.
|
|
|
|
|
Err(gate) => {
|
|
|
|
|
let other = if rule == "t_count" { "t_count_other" } else { "two_qubit_wide" };
|
|
|
|
|
let w = Wrong {
|
|
|
|
|
kind: "constraint",
|
|
|
|
|
text: say(loc, &format!("wrong.{other}"), &[("gate", gate.to_string()), ("limit", limit)]),
|
|
|
|
|
engine: None,
|
|
|
|
|
lines: Vec::new(),
|
|
|
|
|
case: None,
|
|
|
|
|
pointer: say(loc, &format!("pointer.{other}"), &[("gate", gate.to_string())]),
|
|
|
|
|
};
|
|
|
|
|
(false, Value::Null, Some(w))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
"qubits" => {
|
|
|
|
|
let b = &cons["qubits"];
|
|
|
|
|
let n = m["qubits"].as_u64().unwrap_or(0);
|
|
|
|
|
let w = (!within(n, b)).then(|| Wrong {
|
|
|
|
|
kind: "constraint",
|
|
|
|
|
text: say(loc, &format!("wrong.{rule}"), &[("found", n.to_string()), ("limit", b["max"].as_u64().map_or("?".into(), |x| x.to_string()))]),
|
|
|
|
|
text: say(loc, "wrong.qubits", &[("found", n.to_string()), ("limit", b["max"].as_u64().map_or("?".into(), |x| x.to_string()))]),
|
|
|
|
|
engine: None,
|
|
|
|
|
lines: Vec::new(),
|
|
|
|
|
case: None,
|
|
|
|
|
pointer: say(loc, &format!("pointer.{rule}"), &[]),
|
|
|
|
|
pointer: say(loc, "pointer.qubits", &[]),
|
|
|
|
|
});
|
|
|
|
|
(within(n, b), json!(n), w)
|
|
|
|
|
}
|
|
|
|
|
@ -827,10 +1111,14 @@ pub fn check(id: &str, src: &str, job_bytes: u64, loc: &str) -> Result<Value, St
|
|
|
|
|
}
|
|
|
|
|
"estimate" => {
|
|
|
|
|
let b = &cons["estimate"][&subject];
|
|
|
|
|
if estimate_out.is_null() {
|
|
|
|
|
estimate_out = estimate_of(text, loc);
|
|
|
|
|
while estimates.len() <= k {
|
|
|
|
|
estimates.push(estimate_of(measured[estimates.len()].text, loc));
|
|
|
|
|
}
|
|
|
|
|
match estimate_out["counts"][&subject].as_u64() {
|
|
|
|
|
if estimate_out.is_null() {
|
|
|
|
|
estimate_out = estimates[0].clone();
|
|
|
|
|
}
|
|
|
|
|
let e = &estimates[k];
|
|
|
|
|
match e["counts"][&subject].as_u64() {
|
|
|
|
|
Some(n) => {
|
|
|
|
|
let w = (!within(n, b)).then(|| Wrong {
|
|
|
|
|
kind: "constraint",
|
|
|
|
|
@ -845,16 +1133,32 @@ pub fn check(id: &str, src: &str, job_bytes: u64, loc: &str) -> Result<Value, St
|
|
|
|
|
None => (
|
|
|
|
|
false,
|
|
|
|
|
Value::Null,
|
|
|
|
|
Some(Wrong { kind: "constraint", text: say(loc, "wrong.estimate_refused", &[]), engine: estimate_out["error"].as_str().map(str::to_string), lines: Vec::new(), case: None, pointer: say(loc, "pointer.unchecked", &[]) }),
|
|
|
|
|
Some(Wrong { kind: "constraint", text: say(loc, "wrong.estimate_refused", &[]), engine: e["error"].as_str().map(str::to_string), lines: Vec::new(), case: None, pointer: say(loc, "pointer.unchecked", &[]) }),
|
|
|
|
|
),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
_ => continue,
|
|
|
|
|
};
|
|
|
|
|
constraints.push(json!({ "rule": rule, "subject": (!subject.is_empty()).then_some(subject), "text": text_c, "holds": holds, "found": found }));
|
|
|
|
|
if let Some(w) = wrong {
|
|
|
|
|
wrongs.push(w);
|
|
|
|
|
// Where the cases are named, a constraint broken in one says which.
|
|
|
|
|
let wrong = wrong.map(|mut w| {
|
|
|
|
|
if let (Some(c), Some(nm)) = (case, case_name) {
|
|
|
|
|
w.case = Some(*c);
|
|
|
|
|
w.text = say(loc, "wrong.case", &[("name", nm.clone()), ("text", w.text)]);
|
|
|
|
|
}
|
|
|
|
|
w
|
|
|
|
|
});
|
|
|
|
|
let fails = !holds;
|
|
|
|
|
if row.is_none() || fails {
|
|
|
|
|
row = Some((holds, found, wrong));
|
|
|
|
|
}
|
|
|
|
|
if fails {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
let Some((holds, found, wrong)) = row else { continue };
|
|
|
|
|
constraints.push(json!({ "rule": rule, "subject": (!subject.is_empty()).then_some(subject), "text": text_c, "holds": holds, "found": found }));
|
|
|
|
|
if let Some(w) = wrong {
|
|
|
|
|
wrongs.push(w);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
@ -1035,6 +1339,195 @@ mod tests {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn an_exercise_outside_the_course_is_checked_as_a_lesson_is() {
|
|
|
|
|
// A lesson's own exercise, given as a document: the same check, byte for byte, but for
|
|
|
|
|
// the lesson it names (none).
|
|
|
|
|
let (c, i) = find("first-circuit").unwrap();
|
|
|
|
|
let ex = &c.lessons[i].exercise;
|
|
|
|
|
let right = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[1] q;\nbit[1] c;\nx q[0];\nc = measure q;\n";
|
|
|
|
|
let wrong = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[1] q;\nbit[1] c;\nh q[0];\nc = measure q;\n";
|
|
|
|
|
for src in [right, wrong] {
|
|
|
|
|
let mut lesson = check("first-circuit", src, CAP, "en").unwrap();
|
|
|
|
|
let doc = check_exercise(ex, src, CAP, "en").unwrap();
|
|
|
|
|
assert_eq!(doc["lesson"], Value::Null, "{doc:#}");
|
|
|
|
|
lesson["lesson"] = Value::Null;
|
|
|
|
|
assert_eq!(lesson, doc);
|
|
|
|
|
}
|
|
|
|
|
// An exercise written from nothing but what the program must do.
|
|
|
|
|
let mine = json!({ "schema": EXERCISE_SCHEMA, "intent": { "distribution": { "00": 0.5, "11": 0.5 } } });
|
|
|
|
|
let bell = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;\n";
|
|
|
|
|
let ok = check_exercise(&mine, bell, CAP, "en").unwrap();
|
|
|
|
|
assert_eq!((ok["passed"].as_bool(), ok["level"].as_str()), (Some(true), Some("proof")), "{ok:#}");
|
|
|
|
|
let no = check_exercise(&mine, &bell.replace("cx q[0], q[1];\n", ""), CAP, "en").unwrap();
|
|
|
|
|
assert_eq!(no["passed"], false, "{no:#}");
|
|
|
|
|
assert_eq!(no["what_went_wrong"][0]["kind"], "distribution", "{no:#}");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const HEAD: &str = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\n";
|
|
|
|
|
|
|
|
|
|
/// An exercise that is no lesson's is held to the form a lesson's is before
|
|
|
|
|
/// anything runs: a bound that is not whole numbers, a constraint the
|
|
|
|
|
/// course does not name, a job that is not shots, seed and noise, too many
|
|
|
|
|
/// cases or intents, or a field an exercise does not have, is refused, never
|
|
|
|
|
/// read as no bound or dropped. Each program here does what the intent
|
|
|
|
|
/// asks, so a vacuous constraint would let it pass.
|
|
|
|
|
#[test]
|
|
|
|
|
fn a_malformed_exercise_is_refused_before_anything_is_checked() {
|
|
|
|
|
let src = format!("{HEAD}qubit[1] q;\nbit[1] c;\nx q[0];\nt q[0];\nt q[0];\nc = measure q;\n");
|
|
|
|
|
let with = |k: &str, v: Value| {
|
|
|
|
|
let mut ex = json!({ "schema": EXERCISE_SCHEMA, "intent": { "distribution": { "1": 1.0 } } });
|
|
|
|
|
ex[k] = v;
|
|
|
|
|
ex
|
|
|
|
|
};
|
|
|
|
|
// Well formed, it reads and the program passes; bounded, it fails.
|
|
|
|
|
let ok = check_exercise(&with("constraints", json!({})), &src, CAP, "en").unwrap();
|
|
|
|
|
assert_eq!(ok["passed"], true, "{ok:#}");
|
|
|
|
|
let bounded = check_exercise(&with("constraints", json!({ "t_count": { "max": 0 } })), &src, CAP, "en").unwrap();
|
|
|
|
|
assert_eq!((bounded["passed"].as_bool(), bounded["constraints"][0]["found"].as_u64()), (Some(false), Some(2)), "{bounded:#}");
|
|
|
|
|
let bad_constraints = [
|
|
|
|
|
json!({ "t_count": 0 }),
|
|
|
|
|
json!({ "t_count": { "max": -1 } }),
|
|
|
|
|
json!({ "t_count": { "max": 1.5 } }),
|
|
|
|
|
json!({ "t_count": { "max": 0.0 } }),
|
|
|
|
|
json!({ "t_count": { "max": "0" } }),
|
|
|
|
|
json!({ "t_count": { "maximum": 0 } }),
|
|
|
|
|
json!({ "t_count": { "min": 3, "max": 1 } }),
|
|
|
|
|
json!({ "t_count": { "min": 1 } }),
|
|
|
|
|
json!({ "two_qubit": { "min": 0, "max": 3 } }),
|
|
|
|
|
json!({ "estimate": { "toffolis": { "min": 1 } } }),
|
|
|
|
|
json!({ "two_qubit": {} }),
|
|
|
|
|
json!({ "counts": { "CX": { "max": 0 } } }),
|
|
|
|
|
json!({ "counts": 5 }),
|
|
|
|
|
json!({ "estimate": 5 }),
|
|
|
|
|
json!({ "estimate": { "t_gates": { "max": 1.0 } } }),
|
|
|
|
|
json!({ "depth": { "max": 3 } }),
|
|
|
|
|
json!({ "no_smells": "cancels" }),
|
|
|
|
|
json!({ "no_smells": ["everything"] }),
|
|
|
|
|
json!(5),
|
|
|
|
|
];
|
|
|
|
|
let many = |n: usize, one: Value| Value::Array(vec![one; n]);
|
|
|
|
|
let bad: Vec<Value> = bad_constraints
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(|c| with("constraints", c))
|
|
|
|
|
.chain([json!(1), json!("x"), json!([]), json!(true), json!({ "repair": true }), json!({ "dyadic_bits": 3 }), json!({ "compile": { "level": 2 } }), json!({ "shots": 2.5 }), json!({ "shots": 0 }), json!({ "seed": -1 }), json!({ "noise": { "depolarizing_1q": 2 } })].map(|j| with("job", j)))
|
|
|
|
|
.chain([
|
|
|
|
|
with("cases", many(100, json!({ "inputs": {} }))),
|
|
|
|
|
with("intent", many(100, json!({ "distribution": { "1": 1.0 } }))),
|
|
|
|
|
with("cases", json!([{ "inputs": { "err": 3.0 } }])),
|
|
|
|
|
with("cases", json!([{ "inputs": {}, "colour": 1 }])),
|
|
|
|
|
with("cases", json!(5)),
|
|
|
|
|
with("constrains", json!({ "t_count": { "max": 0 } })),
|
|
|
|
|
with("schema", json!("wai.quantum.job/1")),
|
|
|
|
|
json!({ "cases": [{ "inputs": {} }] }),
|
|
|
|
|
json!([1]),
|
|
|
|
|
])
|
|
|
|
|
.collect();
|
|
|
|
|
for ex in bad {
|
|
|
|
|
let r = std::panic::catch_unwind(|| check_exercise(&ex, &src, CAP, "en"));
|
|
|
|
|
let r = r.unwrap_or_else(|_| panic!("the check panicked on {ex}"));
|
|
|
|
|
assert!(r.is_err(), "{ex} was checked, not refused: {:#}", r.unwrap());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// A case's inputs bind: a program that does not declare an input a case
|
|
|
|
|
/// gives is refused there, not run without it. And the constraints are held
|
|
|
|
|
/// in every case, not only the first: a gate one case alone adds is counted.
|
|
|
|
|
#[test]
|
|
|
|
|
fn every_case_binds_its_inputs_and_is_held_to_the_constraints() {
|
|
|
|
|
// The bit-flip code with no flip at all: it would pass every case unbound.
|
|
|
|
|
let no_flip = format!("{HEAD}qubit[3] q;\nbit[1] c;\nx q[0];\nc[0] = measure q[0];\n");
|
|
|
|
|
let c = check("bit-flip-code", &no_flip, CAP, "en").unwrap();
|
|
|
|
|
assert_eq!(c["passed"], false, "{c:#}");
|
|
|
|
|
assert!(c["what_went_wrong"].as_array().unwrap().iter().any(|w| w["text"].as_str().unwrap().contains("Q114")), "{c:#}");
|
|
|
|
|
// Two x gates more in the second case only: the intent holds in both.
|
|
|
|
|
let ex = json!({
|
|
|
|
|
"intent": { "distribution": { "1": 1.0 } },
|
|
|
|
|
"cases": [{ "name": "no flip", "inputs": { "err": 0 } }, { "name": "a flip", "inputs": { "err": 1 } }],
|
|
|
|
|
"constraints": { "counts": { "x": { "max": 1 } } },
|
|
|
|
|
});
|
|
|
|
|
let src = format!("{HEAD}input uint err;\nqubit[1] q;\nbit[1] c;\nx q[0];\nif (err == 1) {{\n x q[0];\n x q[0];\n}}\nc[0] = measure q[0];\n");
|
|
|
|
|
let c = check_exercise(&ex, &src, CAP, "en").unwrap();
|
|
|
|
|
assert!(c["cases"].as_array().unwrap().iter().all(|k| k["passed"] == true), "{c:#}");
|
|
|
|
|
assert_eq!((c["passed"].as_bool(), c["constraints"][0]["holds"].as_bool(), c["constraints"][0]["found"].as_u64()), (Some(false), Some(false), Some(3)), "{c:#}");
|
|
|
|
|
let w = &c["what_went_wrong"][0];
|
|
|
|
|
assert_eq!((w["kind"].as_str(), w["case"].as_u64()), (Some("constraint"), Some(1)), "{c:#}");
|
|
|
|
|
assert!(w["text"].as_str().unwrap().starts_with("With a flip:"), "{w}");
|
|
|
|
|
// Within the bound in every case, it passes, and says what the first case found.
|
|
|
|
|
let ok = check_exercise(&ex, &src.replace(" x q[0];\n x q[0];\n", " z q[0];\n"), CAP, "en").unwrap();
|
|
|
|
|
assert_eq!((ok["passed"].as_bool(), ok["constraints"][0]["found"].as_u64()), (Some(true), Some(1)), "{ok:#}");
|
|
|
|
|
// A case whose intent is null takes the exercise's, as one without does.
|
|
|
|
|
let null = json!({ "intent": { "distribution": { "1": 1.0 } }, "cases": [{ "name": "a", "inputs": {}, "intent": null }] });
|
|
|
|
|
let one = format!("{HEAD}qubit[1] q;\nbit[1] c;\nx q[0];\nc = measure q;\n");
|
|
|
|
|
let c = check_exercise(&null, &one, CAP, "en").unwrap();
|
|
|
|
|
assert_eq!((c["passed"].as_bool(), c["level"].as_str()), (Some(true), Some("proof")), "{c:#}");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// An error a case injects is corrected from the qubits: the cases' programs differ only in
|
|
|
|
|
/// the one x the error is, so a program that declares the error and never applies it, reads
|
|
|
|
|
/// it classically, or flips another qubit does not pass, nor one that keeps its answer on a
|
|
|
|
|
/// qubit the error never reaches.
|
|
|
|
|
#[test]
|
|
|
|
|
fn an_injected_error_is_corrected_from_the_qubits_and_never_read() {
|
|
|
|
|
let inject = |c: &Value| c["constraints"].as_array().unwrap().iter().find(|r| r["rule"] == "inject").cloned().unwrap_or(Value::Null);
|
|
|
|
|
// Declared, never applied: every case's program the same, the answer 1 at once.
|
|
|
|
|
let unused = format!("{HEAD}input uint err;\nqubit[3] q;\nbit[1] c;\nx q[0];\nc[0] = measure q[0];\n");
|
|
|
|
|
let c = check("bit-flip-code", &unused, CAP, "en").unwrap();
|
|
|
|
|
assert_eq!((c["passed"].as_bool(), inject(&c)["holds"].as_bool(), inject(&c)["found"].as_str()), (Some(false), Some(false), Some("unused")), "{c:#}");
|
|
|
|
|
// The syndrome decided on the classical error, not measured from the qubits.
|
|
|
|
|
let oracle = format!("{HEAD}input uint err;\nqubit[5] q;\nbit[2] s;\nx q[err];\nif (err == 0) {{ cx q[0], q[3]; }}\nif (err == 1) {{ cx q[1], q[3]; cx q[1], q[4]; }}\nif (err == 2) {{ cx q[2], q[4]; }}\ns[0] = measure q[3];\ns[1] = measure q[4];\n");
|
|
|
|
|
let c = check("syndrome", &oracle, CAP, "en").unwrap();
|
|
|
|
|
assert!(c["cases"].as_array().unwrap().iter().all(|k| k["passed"] == true), "the oracle's syndromes are right: {c:#}");
|
|
|
|
|
assert_eq!((c["passed"].as_bool(), inject(&c)["found"].as_str()), (Some(false), Some("read")), "{c:#}");
|
|
|
|
|
// Another qubit flipped than the case names.
|
|
|
|
|
let elsewhere = format!("{HEAD}input uint err;\nqubit[3] q;\nbit[1] c;\nx q[0];\ncx q[0], q[1];\ncx q[0], q[2];\nx q[2 - err];\ncx q[0], q[1];\ncx q[0], q[2];\nccx q[2], q[1], q[0];\nc[0] = measure q[0];\n");
|
|
|
|
|
let c = check("bit-flip-code", &elsewhere, CAP, "en").unwrap();
|
|
|
|
|
assert_eq!((c["passed"].as_bool(), inject(&c)["found"].as_str()), (Some(false), Some("where")), "{c:#}");
|
|
|
|
|
// The answer kept on a fourth qubit, which no error reaches.
|
|
|
|
|
let aside = format!("{HEAD}input uint err;\nqubit[4] q;\nbit[1] c;\nx q[3];\nx q[err];\nc[0] = measure q[3];\n");
|
|
|
|
|
let c = check("bit-flip-code", &aside, CAP, "en").unwrap();
|
|
|
|
|
assert_eq!(inject(&c)["holds"], true, "{c:#}");
|
|
|
|
|
assert!(c["passed"] == false && c["constraints"].as_array().unwrap().iter().any(|r| r["rule"] == "qubits" && r["holds"] == false), "{c:#}");
|
|
|
|
|
// The lesson's own solution holds to it.
|
|
|
|
|
let (l, i) = find("bit-flip-code").unwrap();
|
|
|
|
|
let c = check("bit-flip-code", l.lessons[i].exercise["reference"].as_str().unwrap(), CAP, "en").unwrap();
|
|
|
|
|
assert_eq!((c["passed"].as_bool(), inject(&c)["holds"].as_bool()), (Some(true), Some(true)), "{c:#}");
|
|
|
|
|
// An injection the cases cannot make is refused.
|
|
|
|
|
let bad = json!({ "intent": { "distribution": { "1": 1.0 } }, "cases": [{ "inputs": { "err": 0 } }, { "inputs": { "err": 0 } }], "constraints": { "inject": "err" } });
|
|
|
|
|
assert!(check_exercise(&bad, &unused, CAP, "en").unwrap_err().contains("inject"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// A T count counts T gates however they are written: a phase rotation by
|
|
|
|
|
/// an odd multiple of π/4 is a T up to Clifford gates, and a gate whose T
|
|
|
|
|
/// count is not fixed does not hold to a bound on it. A gate on three qubits
|
|
|
|
|
/// is not bounded by a count of two-qubit gates.
|
|
|
|
|
#[test]
|
|
|
|
|
fn a_t_count_counts_t_gates_however_they_are_written() {
|
|
|
|
|
// The t-count lesson's own intent, each T written as p(π/4): the same circuit.
|
|
|
|
|
let (c, i) = find("t-count").unwrap();
|
|
|
|
|
let reference = c.lessons[i].exercise["intent"]["equivalent"].as_str().unwrap();
|
|
|
|
|
let respelled = reference.replace("tdg q[1];", "p(-pi/4) q[1];").replace("t q[0];", "p(pi/4) q[0];").replace("t q[1];", "p(pi/4) q[1];");
|
|
|
|
|
assert!(!respelled.contains("\nt ") && !respelled.contains("tdg q"), "{respelled}");
|
|
|
|
|
let r = check("t-count", &respelled, CAP, "en").unwrap();
|
|
|
|
|
assert_eq!(r["cases"][0]["passed"], true, "the respelling is the same circuit: {r:#}");
|
|
|
|
|
assert_eq!((r["passed"].as_bool(), r["constraints"][0]["found"].as_u64()), (Some(false), Some(8)), "{r:#}");
|
|
|
|
|
let task = |c: Value, intent: &str| json!({ "intent": { "distribution": { intent: 1.0 } }, "constraints": c });
|
|
|
|
|
// A rotation that is no T: its T count is not fixed, so no bound holds.
|
|
|
|
|
let rz = check_exercise(&task(json!({ "t_count": { "max": 0 } }), "0"), &format!("{HEAD}qubit[1] q;\nbit[1] c;\nrz(0.3) q[0];\nc = measure q;\n"), CAP, "en").unwrap();
|
|
|
|
|
assert_eq!((rz["passed"].as_bool(), rz["constraints"][0]["holds"].as_bool()), (Some(false), Some(false)), "{rz:#}");
|
|
|
|
|
assert!(rz["what_went_wrong"][0]["text"].as_str().unwrap().contains("rz gate"), "{rz:#}");
|
|
|
|
|
// A Clifford rotation counts none; T and p(π/4) count one each.
|
|
|
|
|
let mixed = check_exercise(&task(json!({ "t_count": { "max": 1 } }), "0"), &format!("{HEAD}qubit[1] q;\nbit[1] c;\np(pi/2) q[0];\nt q[0];\np(pi/4) q[0];\nc = measure q;\n"), CAP, "en").unwrap();
|
|
|
|
|
assert_eq!((mixed["passed"].as_bool(), mixed["constraints"][0]["found"].as_u64()), (Some(false), Some(2)), "{mixed:#}");
|
|
|
|
|
// A Toffoli is not a two-qubit gate, so a bound on them does not bound it.
|
|
|
|
|
let ccx = format!("{HEAD}qubit[3] q;\nbit[3] c;\nx q[0];\nx q[1];\nccx q[0], q[1], q[2];\nc = measure q;\n");
|
|
|
|
|
let wide = check_exercise(&task(json!({ "two_qubit": { "max": 0 } }), "111"), &ccx, CAP, "en").unwrap();
|
|
|
|
|
assert_eq!((wide["passed"].as_bool(), wide["constraints"][0]["holds"].as_bool()), (Some(false), Some(false)), "{wide:#}");
|
|
|
|
|
assert!(wide["what_went_wrong"][0]["text"].as_str().unwrap().contains("ccx gate"), "{wide:#}");
|
|
|
|
|
let two = check_exercise(&task(json!({ "two_qubit": { "max": 1 } }), "11"), &format!("{HEAD}qubit[2] q;\nbit[2] c;\nx q[0];\ncx q[0], q[1];\nc = measure q;\n"), CAP, "en").unwrap();
|
|
|
|
|
assert_eq!((two["passed"].as_bool(), two["constraints"][0]["found"].as_u64()), (Some(true), Some(1)), "{two:#}");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn a_check_says_what_went_wrong_from_the_engine_and_never_shows_the_solution() {
|
|
|
|
|
// A distribution: the largest gap is named, and the pointer is to it.
|
|
|
|
|
|