From b7ff363cc7b84b906f58dbde42d3b45dcaad720c Mon Sep 17 00:00:00 2001 From: Michele Alberti <michele.alberti@cea.fr> Date: Wed, 16 Nov 2022 18:20:31 +0100 Subject: [PATCH] [verification] Create theory from json config file. --- src/dataset.ml | 74 ++++++++++++++++++++++++---------- src/dataset.mli | 7 +++- src/verification.ml | 85 +++++++++++++++++++++++++++++++++++---- src/verification.mli | 2 +- tests/pyrat_verify_json.t | 18 +++++++++ 5 files changed, 153 insertions(+), 33 deletions(-) create mode 100644 tests/pyrat_verify_json.t diff --git a/src/dataset.ml b/src/dataset.ml index 456981e..32dbe5a 100644 --- a/src/dataset.ml +++ b/src/dataset.ml @@ -23,6 +23,20 @@ open Why3 open Base +let real_constant_of_float value = + let neg = Float.is_negative value in + let value = Fmt.str "%.64f" (Float.abs value) in + (* Split into integer and fractional parts. *) + let int_frac = String.split value ~on:'.' in + let int = List.hd_exn int_frac in + let frac = + match List.tl_exn int_frac with + | [] -> "" + | [ s ] -> s + | _ -> assert false (* Since every float has one '.' at most. *) + in + Constant.ConstReal (Number.real_literal ~radix:10 ~neg ~int ~frac ~exp:None) + let float_of_real_constant rc = Float.of_string (Fmt.str "%a" Number.(print_real_constant full_support) rc) @@ -84,8 +98,39 @@ let interpret_normalization = | _ -> acc) [] +let term_of_normalization env normalization = + let th = Pmodule.read_module env [ "caisar" ] "DataSetClassification" in + let t_dataset = + let ls_dataset = Theory.ns_find_ls th.mod_theory.th_export [ "dataset" ] in + Term.t_app_infer ls_dataset [] + in + let ty = + let th = Env.read_theory env [ "ieee_float" ] "Float64" in + Ty.ty_app (Theory.ns_find_ts th.th_export [ "t" ]) [] + in + match normalization with + | MinMax (min, max) -> + let t_min = Term.t_const (real_constant_of_float min) ty in + let t_max = Term.t_const (real_constant_of_float max) ty in + let ls_min_max_scale = + Theory.ns_find_ls th.mod_theory.th_export [ "min_max_scale" ] + in + Term.t_app_infer ls_min_max_scale [ t_min; t_max; t_dataset ] + | Znorm (mean, std_dev) -> + let t_mean = Term.t_const (real_constant_of_float mean) ty in + let t_std_dev = Term.t_const (real_constant_of_float std_dev) ty in + let ls_z_norm = Theory.ns_find_ls th.mod_theory.th_export [ "z_norm" ] in + Term.t_app_infer ls_z_norm [ t_mean; t_std_dev; t_dataset ] + type eps = float [@@deriving yojson, show] +let string_of_eps eps = Float.to_string eps + +let term_of_eps env eps = + let th = Env.read_theory env [ "ieee_float" ] "Float64" in + let ty = Ty.ty_app (Theory.ns_find_ts th.th_export [ "t" ]) [] in + Term.t_const (real_constant_of_float eps) ty + type property = | Correct | Robust of eps @@ -99,8 +144,6 @@ type ('a, 'b) predicate = { property : property; } -let string_of_eps eps = Float.to_string eps - let string_of_predicate_kind = function | Correct -> "correct" | Robust _ -> "robust" @@ -160,19 +203,6 @@ let clip_value value = let value = if Float.is_negative value then Float.zero else value in if Float.(is_negative (one -. value)) then Float.one else value -let real_constant value = - (* Split into integer and fractional parts. *) - let int_frac = String.split Float.(to_string (abs value)) ~on:'.' in - let int = List.hd_exn int_frac in - let frac = - match List.tl_exn int_frac with - | [] -> "" - | [ s ] -> s - | _ -> assert false (* Since every float has one '.' at most. *) - in - Constant.ConstReal - (Number.real_literal ~radix:10 ~neg:false ~int ~frac ~exp:None) - let add_axioms ?eps ~clip th task input value = let eps = Float.(abs (of_string (Option.value eps ~default:"0"))) in let value = Float.of_string value in @@ -185,7 +215,7 @@ let add_axioms ?eps ~clip th task input value = let t_ge = let value = value -. eps in let value = if clip then clip_value value else value in - let cst = real_constant value in + let cst = real_constant_of_float value in Term.(ps_app le [ t_const cst Ty.ty_real; fs_app input [] Ty.ty_real ]) in let task = Task.add_prop_decl task Decl.Paxiom pr_ge t_ge in @@ -197,7 +227,7 @@ let add_axioms ?eps ~clip th task input value = let t_le = let value = value +. eps in let value = if clip then clip_value value else value in - let cst = real_constant value in + let cst = real_constant_of_float value in Term.(ps_app le [ fs_app input [] Ty.ty_real; t_const cst Ty.ty_real ]) in Task.add_prop_decl task Decl.Paxiom pr_le t_le @@ -217,16 +247,16 @@ let add_output_decl ~id predicate_kind ~outputs ~record th task = (* Add goal formula. *) let class_ = List.hd_exn record in let goal_fmla = - let gt = Theory.(ns_find_ls th.th_export [ Ident.op_infix ">" ]) in - let class_ls = List.nth_exn outputs (Int.of_string class_) in + let ls_gt = Theory.(ns_find_ls th.th_export [ Ident.op_infix ">" ]) in + let ls_class = List.nth_exn outputs (Int.of_string class_) in List.fold outputs ~init:Term.t_true ~f:(fun fmla output -> - if Term.ls_equal class_ls output + if Term.ls_equal ls_class output then fmla else let gt_fmla = Term.( - ps_app gt - [ fs_app class_ls [] Ty.ty_real; fs_app output [] Ty.ty_real ]) + ps_app ls_gt + [ fs_app ls_class [] Ty.ty_real; fs_app output [] Ty.ty_real ]) in Term.t_and fmla gt_fmla) in diff --git a/src/dataset.mli b/src/dataset.mli index e1465f7..f1f2e13 100644 --- a/src/dataset.mli +++ b/src/dataset.mli @@ -24,6 +24,9 @@ open Why3 type eps [@@deriving yojson, show] +val string_of_eps : eps -> string +val term_of_eps : Env.env -> eps -> Term.term + type property = private | Correct | Robust of eps @@ -32,6 +35,8 @@ type property = private type normalization [@@deriving yojson, show] +val term_of_normalization : Env.env -> normalization -> Term.term + type ('model, 'dataset) predicate = private { model : 'model; dataset : 'dataset; @@ -39,8 +44,6 @@ type ('model, 'dataset) predicate = private { property : property; } -val string_of_eps : eps -> string - val interpret_predicate : Env.env -> on_model:(Term.lsymbol -> 'model) -> diff --git a/src/verification.ml b/src/verification.ml index cbe3b35..c802ac8 100644 --- a/src/verification.ml +++ b/src/verification.ml @@ -34,7 +34,7 @@ module File = struct and property = { dataset : string; - normalization : Dataset.normalization list; + normalization : Dataset.normalization option; kind : Dataset.property; } [@@deriving yojson, show] @@ -192,14 +192,83 @@ let call_prover ~limit config env prover config_prover driver dataset_csv task = additional_info) let mstr_theory_of_json_config env (json_config : File.json_config) = - let pmod = Language.nnet_parser env json_config.model in - let th_pmod = (Wstdlib.Mstr.find "NNasArray" pmod).mod_theory in - let name = "T" in + let model_parser = + let extension = Filename.(extension (basename json_config.model)) in + match String.lowercase extension with + | ".nnet" -> Language.nnet_parser + | ".onnx" -> Language.onnx_parser + | ".ovo" -> Language.ovo_parser + | "" -> invalid_arg "Cannot find model extension" + | _ -> invalid_arg (Fmt.str "Unrecognized model extension '%s'" extension) + in + let th_as_array = + let pmod = model_parser env (Unix.realpath json_config.model) in + (Wstdlib.Mstr.find "AsArray" pmod).mod_theory + in + let th_float64 = Env.read_theory env [ "ieee_float" ] "Float64" in + let th_ds_classification = + Pmodule.read_module env [ "caisar" ] "DataSetClassification" + in + let th_ds_props = Pmodule.read_module env [ "caisar" ] "DataSetProps" in + let name = "JSON_config" in let th_uc = Theory.create_theory (Ident.id_fresh name) in - let th_real = Env.read_theory env [ "real" ] "Real" in - let th_uc = Theory.use_export th_uc th_real in - let th_uc = Theory.use_export th_uc th_pmod in - Wstdlib.Mstr.singleton name (Theory.close_theory th_uc) + let th_uc = Theory.use_export th_uc th_as_array in + let th_uc = Theory.use_export th_uc th_float64 in + let th_uc = Theory.use_export th_uc th_ds_classification.mod_theory in + let th_uc = Theory.use_export th_uc th_ds_props.mod_theory in + let t_dataset = + let ls_dataset = + Theory.ns_find_ls th_ds_classification.mod_theory.th_export [ "dataset" ] + in + Term.t_app_infer ls_dataset [] + in + let t_model = + let ls_model = Theory.ns_find_ls th_as_array.th_export [ "model" ] in + Term.t_app_infer ls_model [] + in + (* Add dataset normalization. *) + let t_dataset, th_uc = + match json_config.property.normalization with + | None -> (t_dataset, th_uc) + | Some n -> + let t_normalization = Dataset.term_of_normalization env n in + let ls_dataset_norm = + let ty = + Ty.ty_app + (Theory.ns_find_ts th_ds_classification.mod_theory.th_export + [ "dataset" ]) + [] + in + Term.create_fsymbol (Ident.id_fresh "dataset'") [] ty + in + let ld_dataset_norm = + Decl.make_ls_defn ls_dataset_norm [] t_normalization + in + ( Term.t_app_infer ls_dataset_norm [], + Theory.add_logic_decl th_uc [ ld_dataset_norm ] ) + in + (* Create goal formula about specified property. *) + let th_uc = + let t_property = + match json_config.property.kind with + | Dataset.Correct -> + let ls_correct = + Theory.ns_find_ls th_ds_props.mod_theory.th_export [ "correct" ] + in + Term.t_app_infer ls_correct [ t_model; t_dataset ] + | Robust eps -> + let ls_robust = + Theory.ns_find_ls th_ds_props.mod_theory.th_export [ "robust" ] + in + let t_eps = Dataset.term_of_eps env eps in + Term.t_app_infer ls_robust [ t_model; t_dataset; t_eps ] + | CondRobust _ -> invalid_arg "Unsupported property" + in + let prsymbol = Decl.create_prsymbol (Ident.id_fresh "property") in + Theory.add_prop_decl th_uc Decl.Pgoal prsymbol t_property + in + let th = Theory.close_theory th_uc in + Wstdlib.Mstr.singleton name th let verify ?(debug = false) ?format ~loadpath ?memlimit ?timelimit prover ?dataset_csv file = diff --git a/src/verification.mli b/src/verification.mli index 73d5273..d1f2186 100644 --- a/src/verification.mli +++ b/src/verification.mli @@ -30,7 +30,7 @@ module File : sig and property = private { dataset : string; - normalization : Dataset.normalization list; + normalization : Dataset.normalization option; kind : Dataset.property; } [@@deriving yojson, show] diff --git a/tests/pyrat_verify_json.t b/tests/pyrat_verify_json.t new file mode 100644 index 0000000..aace626 --- /dev/null +++ b/tests/pyrat_verify_json.t @@ -0,0 +1,18 @@ +Test verify-json + $ cat - > test_data.csv << EOF + > 1,0,255,200,5,198 + > EOF + + $ cat - > config.json << EOF + > {"prover":["Pyrat"],"model":"TestNetwork.nnet","property":{"dataset":"test_data.csv","normalization":["MinMax",0.0,255.0],"kind":["Robust",0.01]}} + > EOF + + $ chmod u+x bin/pyrat.py + + $ bin/pyrat.py --version + PyRAT 1.1 + + $ PATH=$(pwd)/bin:$PATH + + $ caisar verify-json config.json 2>&1 <<EOF | ./filter_tmpdir.sh + [caisar] Goal property: Unknown () -- GitLab