chore(compiler): Update security curves

security estimated using the MATZOV cost model in the lattice estimator
 commit: 60808bdc17f99b78bd1bbcd621af04bc6f0e98bb
 (see "Report on the Security of LWE: Improved Dual Lattice Attack", April 2022)
This commit is contained in:
Bourgerie Quentin
2024-06-10 15:23:38 +02:00
parent 2700f60f0a
commit eba61f20cd
20 changed files with 5 additions and 720 deletions

View File

@@ -1,69 +0,0 @@
LATTICE_ESTIMATOR_DIR=$(PWD)/../../third_party/lattice-estimator
SECURITY_LEVELS=80 112 128 192
SAGE_OBJECT_DIR=sage-object
SAGE_SECURITY_CURVES=$(SECURITY_LEVELS:%=$(SAGE_OBJECT_DIR)/%.sobj)
SAGE_VERIFIED_CURVES=$(SAGE_OBJECT_DIR)/verified_curves.sobj
CURVES_JSON_PATH=json/curves.json
CURVES_CPP_GEN_H=concrete-security-curves-cpp/include/concrete/curves.gen.h
CURVES_RUST_GEN_TXT=concrete-security-curves-rust/src/gaussian/curves_gen.rs
generate-code: generate-cpp generate-rust
# Generate CPP ########################
$(CURVES_CPP_GEN_H): concrete-security-curves-cpp/gen_header.py $(CURVES_JSON_PATH)
cat $(CURVES_JSON_PATH) | python3 concrete-security-curves-cpp/gen_header.py > $(CURVES_CPP_GEN_H)
generate-cpp: $(CURVES_CPP_GEN_H)
# Generate RUST ########################
$(CURVES_RUST_GEN_TXT): concrete-security-curves-rust/gen_table.py $(CURVES_JSON_PATH)
cat $(CURVES_JSON_PATH) | python3 concrete-security-curves-rust/gen_table.py > $(CURVES_RUST_GEN_TXT)
generate-rust: $(CURVES_RUST_GEN_TXT)
# Compare curves #######################
$(SAGE_OBJECT_DIR)/outdated_curves.timestamp: ./lattice-scripts/compare_curves_and_estimator.py
PYTHONPATH=$(LATTICE_ESTIMATOR_DIR) python3 ./lattice-scripts/compare_curves_and_estimator.py \
--curves-dir $(SAGE_OBJECT_DIR) --security-levels $(SECURITY_LEVELS) --log-q 64 \
&& touch $(SAGE_OBJECT_DIR)/outdated_curves.timestamp
compare-curves: $(SAGE_OBJECT_DIR)/outdated_curves.timestamp
# Compare curves custom q #######################
# run via e.g:
# logq=128 make compare-curves-custom-q
# for q = 2**(128)
$(SAGE_OBJECT_DIR)/outdated_curves.timestamp: ./lattice-scripts/compare_curves_and_estimator.py
PYTHONPATH=$(LATTICE_ESTIMATOR_DIR) python3 ./lattice-scripts/compare_curves_and_estimator.py \
--curves-dir $(SAGE_OBJECT_DIR) --security-levels $(SECURITY_LEVELS) --log-q $(logq) \
&& touch $(SAGE_OBJECT_DIR)/outdated_curves.timestamp
compare-curves-custom-q: $(SAGE_OBJECT_DIR)/outdated_curves.timestamp
# Generate curves ######################
$(SAGE_OBJECT_DIR)/%.sobj: $(SAGE_OBJECT_DIR)/outdated_curves.timestamp ./lattice-scripts/generate_data.sh ./lattice-scripts/generate_data.py
PYTHONPATH=$(LATTICE_ESTIMATOR_DIR) ./lattice-scripts/generate_data.sh \
$* --output $(SAGE_OBJECT_DIR) --old-models $(SAGE_VERIFIED_CURVES)
generate-curves: $(SAGE_SECURITY_CURVES)
# Verify curves #######################
$(CURVES_JSON_PATH) $(SAGE_VERIFIED_CURVES): ./lattice-scripts/verify_curves.py #$(SAGE_SECURITY_CURVES)
python3 ./lattice-scripts/verify_curves.py \
--curves-dir $(SAGE_OBJECT_DIR) --verified-curves-path $(SAGE_VERIFIED_CURVES) \
--security-levels $(SECURITY_LEVELS) --log-q 64 > $(CURVES_JSON_PATH)
verify-curves: $(CURVES_JSON_PATH)
.PHONY: generate-cpp \
generate-rust \
generate-code \
compare-curves \
generate-curves \
verify-curves

View File

@@ -1,43 +0,0 @@
=========
Parameter Curves
=========
This folder contains the code used to choose secure parameters using the Lattice-Estimator_. In particular, we use data obtained from calls to the lattice estimator to generate parameter curves of the form: ``sigma(n) = a * n + b`` which can then be used to choose a suitable error standard deviation `sigma` for a given LWE dimension n.
Usage
---------
To generate the raw data from the lattice estimator, use::
make generate-curves
by default, this script will generate parameter curves for {80, 112, 128, 192} bits of security, using ``log_2(q) = 64``.
To compare the current curves with the output of the lattice estimator, use::
make compare-curves
this will compare the four curves generated above against the output of the version of the lattice estimator found in /third_party.
To generate the associated cpp and rust code, use::
make generate-code
further advanced options can be found inside the Makefile.
Current curves
---------
Current versions of the curves can be found in the ``sage-object`` folder_. To view the raw data used to generate a curve, load one of the files contained in the director sage-object in Sagemath::
sage: X = load("128.sobj")
entries are tuples of the form: ``(n, log_2(q), log_2(sd), \lambda)``. We can view individual entries via::
sage: X["128"][0]
(2366, 64.0, 4.0, 128.51)
.. _Lattice-Estimator: https://github.com/malb/lattice-estimator
.. _folder: https://github.com/zama-ai/concrete/tree/main/tools/parameter-curves/sage-object

View File

@@ -1,13 +0,0 @@
import sys, json;
def print_curve(data):
print(f'\tSecurityCurve({data["security_level"]},{data["slope"]}, {data["bias"]}, {data["minimal_lwe_dimension"]}, KeyFormat::BINARY),')
def print_cpp_curves_declaration(datas):
print("SecurityCurve curves[] = {")
for data in datas:
print_curve(data)
print("};\n")
print(f"size_t curvesLen = {len(data)};")
print_cpp_curves_declaration(json.load(sys.stdin))

View File

@@ -1,8 +1,5 @@
SecurityCurve curves[] = {
SecurityCurve(80,-0.04045822621883835, 1.7183812000404686, 450, KeyFormat::BINARY),
SecurityCurve(112,-0.029881371645803536, 2.6539316216894946, 450, KeyFormat::BINARY),
SecurityCurve(128,-0.026599462343105267, 2.981543184145991, 450, KeyFormat::BINARY),
SecurityCurve(192,-0.018894148763647572, 4.2700349965659115, 532, KeyFormat::BINARY),
SecurityCurve(128, -0.025696778711484593, 2.675931372549016, 450, KeyFormat::BINARY),
};
size_t curvesLen = 4;
size_t curvesLen = 1;

View File

@@ -1,14 +0,0 @@
import sys, json;
def print_curve(data):
print(f' ({data["security_level"]}, SecurityWeights {{ slope: {data["slope"]}, bias: {data["bias"]}, minimal_lwe_dimension: {data["minimal_lwe_dimension"]} }}),')
def print_rust_curves_declaration(datas):
print("use super::security_weights::SecurityWeights;")
print(f"pub const SECURITY_WEIGHTS_ARRAY: [(u64, SecurityWeights); {len(datas)}] = [")
for data in datas:
print_curve(data)
print("];")
print_rust_curves_declaration(json.load(sys.stdin))

View File

@@ -1,7 +1,4 @@
use super::security_weights::SecurityWeights;
pub const SECURITY_WEIGHTS_ARRAY: [(u64, SecurityWeights); 4] = [
(80, SecurityWeights { slope: -0.04045822621883835, bias: 1.7183812000404686, minimal_lwe_dimension: 450 }),
(112, SecurityWeights { slope: -0.029881371645803536, bias: 2.6539316216894946, minimal_lwe_dimension: 450 }),
(128, SecurityWeights { slope: -0.026599462343105267, bias: 2.981543184145991, minimal_lwe_dimension: 450 }),
(192, SecurityWeights { slope: -0.018894148763647572, bias: 4.2700349965659115, minimal_lwe_dimension: 532 }),
pub const SECURITY_WEIGHTS_ARRAY: [(u64, SecurityWeights); 1] = [
(128, SecurityWeights { slope: -0.025696778711484593, bias: 2.675931372549016, minimal_lwe_dimension: 450 }),
];

View File

@@ -1 +0,0 @@
[{"slope": -0.04045822621883835, "bias": 1.7183812000404686, "security_level": 80, "minimal_lwe_dimension": 450}, {"slope": -0.029881371645803536, "bias": 2.6539316216894946, "security_level": 112, "minimal_lwe_dimension": 450}, {"slope": -0.026599462343105267, "bias": 2.981543184145991, "security_level": 128, "minimal_lwe_dimension": 450}, {"slope": -0.018894148763647572, "bias": 4.2700349965659115, "security_level": 192, "minimal_lwe_dimension": 532}]

View File

@@ -1,139 +0,0 @@
import os
import sys
from estimator import LWE, ND, RC
from sage.all import oo, load, floor, ceil
from generate_data import estimate, get_security_level
import argparse
LOG_N_MAX = 17 + 1
LOG_N_MIN = 10
def get_index(sec, curves):
"""
Retrieve the index of the curve corresponding to the right security :sec:
:param sec: security level
:param curves: output of `generate_and_verify`
:return: index of the right curve
"""
# TODO: Duplicated code from verify_curve
for i in range(len(curves)):
if curves[i][2] == sec:
return i
def estimate_security_with_lattice_estimator(lwe_dimension, std_dev, log_q):
"""
Return the security of (lwe_dimension, std_dev, log_q) as estimated by the latest
version of the lattice estimator
:param lwe_dimension:
:param std_dev:
:param log_q:
:return:
"""
params = LWE.Parameters(
n=lwe_dimension, q=2 ** log_q, Xs=ND.UniformMod(2), Xe=ND.DiscreteGaussian(std_dev), m=oo, tag="params"
)
costs = estimate(params, red_cost_model = RC.BDGL16)
return get_security_level(costs, 2)
def get_minimal_lwe_dimension(curve, security_level, log_q):
"""
Retrieve the smallest lwe dimension usable for the given security level and log_q
:param curve:
:param security_level:
:param log_q:
:return:
"""
minimal_lwe_dim = curve[-1]
return minimal_lwe_dim
def estimate_stddev_with_current_curve(curve, lwe_dimension, log_q):
"""
Use the current formula to estimate the secure noise from the lwe_dimension
:param curve:
:param lwe_dimension:
:param log_q:
:return:
"""
def minimal_stddev(a, b, lwe_dim):
return 2. ** max(ceil(a * lwe_dim + b), 2)
a = curve[0]
b = curve[1] + log_q
stddev = minimal_stddev(a, b, lwe_dimension)
return stddev
def compare_curve_and_estimator(security_level, log_q, curves_dir):
"""
For a subset of every lwe dimension possibles, estimate the security of those lwe dimension
associated with the stddev recommended by our current curve.
Test whether some (lwe_dimension, std dev) that are assumed to be secure with
the current curves are
:param security_level:
:param log_q:
:return: If one of (lwe dim, std dev) is estimated to be less secure than our target `security_level`
this function return False, else return True
"""
print(f"Security Target: {security_level} bits")
# step 0. loading the right curve
curves = load(os.path.join(curves_dir, "verified_curves.sobj"))
j = get_index(security_level, curves)
curve = curves[j]
# step 1. define range of lwe dimensions
n_min = curve[-1]
n_min = max(2 * security_level, 450, n_min)
# TODO: REMOVE HARDCODED 10
lwe_dimensions = list(range(n_min, 1024, 10)) + [2**i for i in range(LOG_N_MIN, LOG_N_MAX)]
# step 2. check security of those points
for lwe_dim in lwe_dimensions:
print("-------------------------")
# (i) get stddev with current curves
predicted_stddev = estimate_stddev_with_current_curve(curve, lwe_dim, log_q)
# (ii) estimate up-to-date security
predicted_security = estimate_security_with_lattice_estimator(lwe_dim, predicted_stddev, log_q)
print("-------------------------")
print(f"lwe dim: {lwe_dim}")
print(f"stddev: {predicted_stddev}")
print(f"Security: {predicted_security}")
if predicted_security < security_level:
return False
return True
if __name__ == "__main__":
CLI = argparse.ArgumentParser()
CLI.add_argument(
"--curves-dir",
help="The directory where curves has been saved (sage object)",
type=str,
required=True,
)
CLI.add_argument(
"--security-levels",
help="The security levels to verify",
nargs="+",
type=int,
required=True
)
CLI.add_argument(
"--log-q",
type=int,
required=True
)
args = CLI.parse_args()
for security_level in args.security_levels:
if not(compare_curve_and_estimator(security_level, args.log_q, args.curves_dir)):
exit(1)
exit(0)

View File

@@ -1,238 +0,0 @@
from sage.all import oo, save, load
from math import log2
import multiprocessing
import argparse
import os
import sys
from estimator import RC, LWE, ND
old_models_sobj = ""
def old_models(security_level, sd, logq=32):
"""
Use the old model as a starting point for the data gathering step
:param security_level: the security level under consideration
:param sd : the standard deviation of the LWE error distribution Xe
:param logq : the (base 2 log) value of the LWE modulus q
"""
def evaluate_model(a, b, stddev=sd):
return (stddev - b) / a
def get_index(sec, curves):
for i in range(len(curves)):
if curves[i][2] == sec:
return i
if old_models_sobj is None or not(os.path.exists(old_models_sobj)):
return 450
curves = load(old_models_sobj)
j = get_index(security_level, curves)
a = curves[j][0]
b = curves[j][1] + logq
n_est = evaluate_model(a, b, sd)
return round(n_est)
def estimate(params, red_cost_model=RC.BDGL16, skip=("arora-gb", "bkw")):
"""
Retrieve an estimate using the Lattice Estimator, for a given set of input parameters
:param params: the input LWE parameters
:param red_cost_model: the lattice reduction cost model
:param skip: attacks to skip
"""
est = LWE.estimate(params, red_cost_model=red_cost_model, deny_list=skip)
return est
def get_security_level(est, dp=2):
"""
Get the security level lambda from a Lattice Estimator output
:param est: the Lattice Estimator output
:param dp: the number of decimal places to consider
"""
attack_costs = []
# note: key does not need to be specified est vs est.keys()
for key in est:
attack_costs.append(est[key]["rop"])
# get the security level correct to 'dp' decimal places
security_level = round(log2(min(attack_costs)), dp)
return security_level
def inequality(x, y):
"""A utility function which compresses the conditions x < y and x > y into a single condition via a multiplier
:param x: the LHS of the inequality
:param y: the RHS of the inequality
"""
if x <= y:
return 1
if x > y:
return -1
def automated_param_select_n(params, target_security=128):
"""A function used to generate the smallest value of n which allows for
target_security bits of security, for the input values of (params.Xe.stddev,params.q)
:param params: the standard deviation of the error
:param target_security: the target number of bits of security, 128 is default
EXAMPLE:
sage: X = automated_param_select_n(Kyber512, target_security = 128)
sage: X
456
"""
# get an estimate based on the prev. model
print("n = {}".format(params.n))
n_start = old_models(target_security, log2(params.Xe.stddev), log2(params.q))
# n_start = max(n_start, 450)
# TODO: think about throwing an error if the required n < 450
params = params.updated(n=n_start)
costs2 = estimate(params)
security_level = get_security_level(costs2, 2)
z = inequality(security_level, target_security)
# we keep n > 2 * target_security as a rough baseline for mitm security
# (on binary key guessing)
while z * security_level < z * target_security:
# TODO: fill in this case! For n > 1024 we only need to consider every
# 256 (optimization)
params = params.updated(n=params.n + z * 8)
costs = estimate(params)
security_level = get_security_level(costs, 2)
if -1 * params.Xe.stddev > 0:
print("target security level is unattainable")
break
# final estimate (we went too far in the above loop)
if security_level < target_security:
# we make n larger
print("we make n larger")
params = params.updated(n=params.n + 8)
costs = estimate(params)
security_level = get_security_level(costs, 2)
print(
"the finalised parameters are n = {}, log2(sd) = {}, log2(q) = {}, with a security level of {}-bits".format(
params.n, log2(params.Xe.stddev), log2(params.q), security_level
)
)
if security_level < target_security:
params.updated(n=None)
return params, security_level
def generate_parameter_matrix(
params_in, sd_range, target_security_levels=[128], name="default_name"
):
"""
:param params_in: a initial set of LWE parameters
:param sd_range: a tuple (sd_min, sd_max) giving the values of sd for which to generate parameters
:param target_security_levels: a list of the target number of bits of security, 128 is default
:param name: a name to save the file
"""
(sd_min, sd_max) = sd_range
for lam in target_security_levels:
for sd in range(sd_min, sd_max + 1):
print(f"run for {lam} {sd}")
Xe_new = ND.DiscreteGaussian(2 ** sd)
(params_out, sec) = automated_param_select_n(
params_in.updated(Xe=Xe_new), target_security=lam
)
try:
results = load("{}.sobj".format(name))
except BaseException:
results = dict()
results["{}".format(lam)] = []
results["{}".format(lam)].append(
(params_out.n, log2(params_out.q), log2(params_out.Xe.stddev), sec)
)
save(results, "{}.sobj".format(name))
return results
def generate_zama_curves64(
sd_range=[2, 58], target_security_levels=[128], name="default_name"
):
"""
The top level function which we use to run the experiment
:param sd_range: a tuple (sd_min, sd_max) giving the values of sd for which to generate parameters
:param target_security_levels: a list of the target number of bits of security, 128 is default
:param name: a name to save the file
"""
if __name__ == "__main__":
D = ND.DiscreteGaussian
vals = range(sd_range[0], sd_range[1])
pool = multiprocessing.Pool(2)
init_params = LWE.Parameters(
n=1024, q=2 ** 64, Xs=D(0.50, -0.50), Xe=D(2 ** 55), m=oo, tag="params"
)
inputs = [
(init_params, (val, val), target_security_levels, name) for val in vals
]
_res = pool.starmap(generate_parameter_matrix, inputs)
return "done"
if __name__ == "__main__":
CLI = argparse.ArgumentParser()
CLI.add_argument(
"--security-level",
type=int,
required=True,
)
CLI.add_argument(
"--output",
type=str,
required=True,
)
CLI.add_argument(
"--old-models",
type=str,
)
CLI.add_argument(
"--sd-min",
type=int,
required=True,
)
CLI.add_argument(
"--sd-max",
type=int,
required=True,
)
CLI.add_argument(
"--margin",
type=int,
default=0,
)
args = CLI.parse_args()
# The script runs the following commands
# grab values of the command-line input arguments
security = args.security_level
sd_min = args.sd_min
sd_max = args.sd_max
margin = args.margin
output = args.output
old_models_sobj = args.old_models
# run the code
generate_zama_curves64(sd_range=(sd_min, sd_max), target_security_levels=[security + margin], name="security_{}_margin_{} ".format(security, margin))

View File

@@ -1,42 +0,0 @@
#!/bin/sh
set -e
output_dir=""
old_models=""
while :
do
case $1 in
--help)
echo "generate_data.sh -o [output_dir] [security_levels]"
exit 2
;;
--output)
output_dir="$2"
shift 2
;;
--old-models)
old_models="$2"
shift 2
;;
--)
break;
;;
"")
break
;;
*)
security_levels="$security_levels $1"
shift;
;;
esac
done
for security_level in $security_levels; do
sage lattice-scripts/generate_data.py --output $output_dir/$security_level.sobj --old-models $old_models --security-level $security_level --sd-min 2 --sd-max 12 --margin 0
sage lattice-scripts/generate_data.py --output $output_dir/$security_level.sobj --old-models $old_models --security-level $security_level --sd-min 12 --sd-max 22 --margin 0
sage lattice-scripts/generate_data.py --output $output_dir/$security_level.sobj --old-models $old_models --security-level $security_level --sd-min 22 --sd-max 32 --margin 0
sage lattice-scripts/generate_data.py --output $output_dir/$security_level.sobj --old-models $old_models --security-level $security_level --sd-min 32 --sd-max 42 --margin 0
sage lattice-scripts/generate_data.py --output $output_dir/$security_level.sobj --old-models $old_models --security-level $security_level --sd-min 42 --sd-max 52 --margin 0
sage lattice-scripts/generate_data.py --output $output_dir/$security_level.sobj --old-models $old_models --security-level $security_level --sd-min 52 --sd-max 59 --margin 0
done

View File

@@ -1,134 +0,0 @@
import numpy as np
from sage.all import save, load, ceil
import json
import os
import argparse
def sort_data(security_level, curves_dir):
from operator import itemgetter
# step 1. load the data
X = load(os.path.join(curves_dir, f"{security_level}.sobj"))
# step 2. sort by SD
x = sorted(X["{}".format(security_level)], key=itemgetter(2))
# step3. replace the sorted value
X["{}".format(security_level)] = x
return X
def generate_curve(security_level, curves_dir):
# step 1. get the data
X = sort_data(security_level, curves_dir)
# step 2. group the n and sigma data into lists
N = []
SD = []
for x in X["{}".format(security_level)]:
N.append(x[0])
SD.append(x[2] + 0.5)
# step 3. perform interpolation and return coefficients
(a, b) = np.polyfit(N, SD, 1)
return a, b
def verify_curve(security_level, a, b, curves_dir):
# step 1. get the table and max values of n, sd
X = sort_data(security_level, curves_dir)
n_max = X["{}".format(security_level)][0][0]
# step 2. a function to get model values
def f_model(a, b, n):
return ceil(a * n + b)
# step 3. a function to get table values
def f_table(table, n):
for i in range(len(table)):
n_val = table[i][0]
if n < n_val:
pass
else:
j = i
break
# now j is the correct index, we return the corresponding sd
return table[j][2]
# step 3. for each n, check whether we satisfy the table
n_min = max(2 * security_level, 450, X["{}".format(security_level)][-1][0])
for n in range(n_max, n_min, -1):
model_sd = f_model(a, b, n)
table_sd = f_table(X["{}".format(security_level)], n)
#print(n, table_sd, model_sd, model_sd >= table_sd)
if table_sd > model_sd:
#print("MODEL FAILS at n = {}".format(n))
return False
return True, n_min
def generate_and_verify(security_levels, log_q, curves_dir, verified_curves_path):
success = []
json = []
fail = []
for sec in security_levels:
# generate the model for security level sec
(a_sec, b_sec) = generate_curve(sec, curves_dir)
# verify the model for security level sec
(status, n_alpha) = verify_curve(sec, a_sec, b_sec, curves_dir)
# append the information into a list
if status:
json.append({"slope": a_sec, "bias": b_sec - log_q, "security_level": sec, "minimal_lwe_dimension": n_alpha})
success.append((a_sec, b_sec - log_q, sec, a_sec, b_sec))
else:
fail.append(sec)
save(success, verified_curves_path)
return json, fail
if __name__ == "__main__":
CLI = argparse.ArgumentParser()
CLI.add_argument(
"--verified-curves-path",
help="The path to store the verified curves (sage object)",
type=str,
required=True,
)
CLI.add_argument(
"--curves-dir",
help="The directory where curves has been saved (sage object)",
type=str,
required=True,
)
CLI.add_argument(
"--security-levels",
help="The security levels to verify",
nargs="+",
type=int,
required=True
)
CLI.add_argument(
"--log-q",
type=int,
required=True
)
args = CLI.parse_args()
(success, fail) = generate_and_verify(args.security_levels, log_q=args.log_q, curves_dir=args.curves_dir, verified_curves_path=args.verified_curves_path)
if (fail):
print("FAILURE: Fail to verify the following curves")
print(json.dumps(fail))
exit(1)
print(json.dumps(success))

View File

@@ -1,3 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="311px" height="283px" viewBox="-0.5 -0.5 311 283" content="&lt;mxfile host=&quot;app.diagrams.net&quot; modified=&quot;2021-08-13T09:23:09.119Z&quot; agent=&quot;5.0 (Macintosh)&quot; etag=&quot;KWBmlER1U8BgVACrMNTv&quot; version=&quot;14.8.6&quot; type=&quot;device&quot;&gt;&lt;diagram id=&quot;8zlkHKYxUkOq8ADBIQsa&quot; name=&quot;Page-1&quot;&gt;5ZZdb9sgFIZ/jS8r2YCpe7mmXSdNk6r2ouvuCBzbaNhEmMRJf/1Ijb+7j3RK1KpXhpfDC+c8GBHgRbG9MWyVf9MCVIBCsQ3wVYBQRBB1n72ya5QEhY2QGSl8UC/cyyfwYhu2lgKqUaDVWlm5GotclyVwO9KYMboeh6VajVddsQxmwj1naq4+SGHzNovzXv8CMsvblSN60YwUrA32mVQ5E7oeSPg6wAujtW1axXYBal+8ti7NvM+/Ge02ZqC0/zIhrflXdlewux83j6uNUY/1982Zd9kwtfYJ+83aXVsBKMWnfSFdr9SlEy9zWyjXi1yzskb/7Crjcrps5oOYlbXfZ9Rl744N6AKs2bmQuq9v7GuWD0rbagYUs3Iztmcec9bZdSvcaukWRqE/kiT0Pv5AEhKOLSq9Nhz8rGE9/2IU0YmRZSYDOzNyjUHavfSM6wB06MOho8mR0M2MjowOH4SOK1ZVkv+JHnZKKpVaaKXNswFOEw6cd5GDkWUSE0fk7fMmaIwJX7ySN8WTXzU5LW9yCt5pil7mLeiSxvQ98J5g6n7Tg3lPLgo0NToy7/gEvEUMiSAv8U7QEtN3wZtMeKNX8j6f3OfdBv+bt+v2L7QmvH/n4utf&lt;/diagram&gt;&lt;/mxfile&gt;"><defs/><g><path d="M 1 281 L 1 1" fill="none" stroke="#000000" stroke-width="2" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 281 281 L 1 281" fill="none" stroke="#000000" stroke-width="2" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 21 231 L 223.86 28.14" fill="none" stroke="#b85450" stroke-width="3" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 228.63 23.37 L 225.45 32.92 L 223.86 28.14 L 219.08 26.55 Z" fill="#b85450" stroke="#b85450" stroke-width="3" stroke-miterlimit="10" pointer-events="all"/><path d="M 31 241 L 273.11 47.31" fill="none" stroke="#d6b656" stroke-width="3" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 278.38 43.1 L 274.16 52.23 L 273.11 47.31 L 268.54 45.2 Z" fill="#d6b656" stroke="#d6b656" stroke-width="3" stroke-miterlimit="10" pointer-events="all"/><path d="M 41 261 L 292.54 96.53" fill="none" stroke="#82b366" stroke-width="3" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 298.19 92.84 L 293.12 101.53 L 292.54 96.53 L 288.2 93.99 Z" fill="#82b366" stroke="#82b366" stroke-width="3" stroke-miterlimit="10" pointer-events="all"/></g></svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -1,2 +0,0 @@
[tool.ruff]
line-length = 169

View File

@@ -1,2 +0,0 @@
xœUÏ[kA‡ñ´<C393>ÖS[µj­ñ­ñmÚ®¶jëBˆï…ì<E280A6>hÜF¶˜¤ù§Û J*2UðBÑSDAŠ ŠÊ¼Lg´<67>8Ã0óž™zà*|‰„S^,UjIg¾ZH‹Þ\¾ZÍ׌'_ÌW
øÿ<EFBFBD>³^­R0°>ŸïfÁ‡"„VÐö«½ Ú²Ùì¥ßjü<6A>,Ÿ‡È

View File

@@ -1,9 +0,0 @@
[(-0.04042633119364589, 1.6609788641436722, 80, 'PASS', 450),
(-0.03414780360867051, 2.017310258660345, 96, 'PASS', 450),
(-0.029670137081135885, 2.162463714083856, 112, 'PASS', 450),
(-0.02640502876522622, 2.4826422691043177, 128, 'PASS', 450),
(-0.023821437305989134, 2.7177789440636673, 144, 'PASS', 450),
(-0.02174358218716036, 2.938810548493322, 160, 'PASS', 498),
(-0.019904056582117684, 2.8161252801542247, 176, 'PASS', 551),
(-0.018610403247590085, 3.2996236848399008, 192, 'PASS', 606),
(-0.014606812351714953, 3.8493629234693003, 256, 'PASS', 826)]