Skip to content

Commit e38f08a

Browse files
committed
Add './mfc.sh packer' command
1 parent f869090 commit e38f08a

File tree

9 files changed

+326
-219
lines changed

9 files changed

+326
-219
lines changed

toolchain/mfc.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,17 @@
66
from mfc.state import ARG
77
from mfc.run import run
88
from mfc.test import test
9+
from mfc.packer import packer
910
from mfc.common import MFC_LOGO, MFCException, quit, format_list_to_string, does_command_exist
1011
from mfc.printer import cons
1112

12-
1313
def __print_greeting():
1414
MFC_LOGO_LINES = MFC_LOGO.splitlines()
1515
max_logo_line_length = max([ len(line) for line in MFC_LOGO_LINES ])
1616

1717
host_line = f"{getpass.getuser()}@{platform.node()} [{platform.system()}]"
1818
targets_line = f"[bold]--targets {format_list_to_string(ARG('targets'), 'magenta', 'None')}[/bold]"
19-
help_line = "$ ./mfc.sh \[build, run, test, clean, count] --help"
19+
help_line = "$ ./mfc.sh \[build, run, test, clean, count, packer] --help"
2020

2121
MFC_SIDEBAR_LINES = [
2222
"",
@@ -48,8 +48,9 @@ def __checks():
4848

4949

5050
def __run():
51-
{"test": test.test, "run": run.run, "build": build.build,
52-
"clean": build.clean, "bench": bench.bench, "count": count.count
51+
{"test": test.test, "run": run.run, "build": build.build,
52+
"clean": build.clean, "bench": bench.bench, "count": count.count,
53+
"packer": packer.packer
5354
}[ARG("command")]()
5455

5556

toolchain/mfc/args.py

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import re, argparse, dataclasses
1+
import re, os.path, argparse, dataclasses
22

33
from .build import get_mfc_target_names, get_target_names, get_dependencies_names
44
from .common import format_list_to_string
55
from .test.test import CASES as TEST_CASES
6-
6+
from .packer import packer
77

88
def parse(config):
99
from .run.engines import ENGINES
@@ -19,14 +19,25 @@ def parse(config):
1919
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
2020
)
2121

22-
parsers = parser.add_subparsers(dest="command")
22+
parsers = parser.add_subparsers(dest="command")
23+
run = parsers.add_parser(name="run", help="Run a case with MFC.", formatter_class=argparse.ArgumentDefaultsHelpFormatter)
24+
test = parsers.add_parser(name="test", help="Run MFC's test suite.", formatter_class=argparse.ArgumentDefaultsHelpFormatter)
25+
build = parsers.add_parser(name="build", help="Build MFC and its dependencies.", formatter_class=argparse.ArgumentDefaultsHelpFormatter)
26+
clean = parsers.add_parser(name="clean", help="Clean build artifacts.", formatter_class=argparse.ArgumentDefaultsHelpFormatter)
27+
bench = parsers.add_parser(name="bench", help="Benchmark MFC (for CI).", formatter_class=argparse.ArgumentDefaultsHelpFormatter)
28+
count = parsers.add_parser(name="count", help="Count LOC in MFC.", formatter_class=argparse.ArgumentDefaultsHelpFormatter)
29+
packer = parsers.add_parser(name="packer", help="Packer utility (pack/unpack/compare)", formatter_class=argparse.ArgumentDefaultsHelpFormatter)
30+
31+
packers = packer.add_subparsers(dest="packer")
32+
pack = packers.add_parser(name="pack", help="Pack a case into a single file.", formatter_class=argparse.ArgumentDefaultsHelpFormatter)
33+
pack.add_argument("input", metavar="INPUT", type=str, default="", help="Input file of case to pack.")
34+
pack.add_argument("-o", "--output", metavar="OUTPUT", type=str, default=None, help="Base name of output file.")
2335

24-
run = parsers.add_parser(name="run", help="Run a case with MFC.", formatter_class=argparse.ArgumentDefaultsHelpFormatter)
25-
test = parsers.add_parser(name="test", help="Run MFC's test suite.", formatter_class=argparse.ArgumentDefaultsHelpFormatter)
26-
build = parsers.add_parser(name="build", help="Build MFC and its dependencies.", formatter_class=argparse.ArgumentDefaultsHelpFormatter)
27-
clean = parsers.add_parser(name="clean", help="Clean build artifacts.", formatter_class=argparse.ArgumentDefaultsHelpFormatter)
28-
bench = parsers.add_parser(name="bench", help="Benchmark MFC (for CI).", formatter_class=argparse.ArgumentDefaultsHelpFormatter)
29-
count = parsers.add_parser(name="count", help="Count LOC in MFC.", formatter_class=argparse.ArgumentDefaultsHelpFormatter)
36+
compare = packers.add_parser(name="compare", help="Compare two cases.", formatter_class=argparse.ArgumentDefaultsHelpFormatter)
37+
compare.add_argument("input1", metavar="INPUT1", type=str, default=None, help="First pack file.")
38+
compare.add_argument("input2", metavar="INPUT2", type=str, default=None, help="Second pack file.")
39+
compare.add_argument("-rel", "--reltol", metavar="RELTOL", type=float, default=1e-12, help="Relative tolerance.")
40+
compare.add_argument("-abs", "--abstol", metavar="ABSTOL", type=float, default=1e-12, help="Absolute tolerance.")
3041

3142
def add_common_arguments(p, mask = None):
3243
if mask is None:
@@ -117,8 +128,8 @@ def append_defaults_to_data(name: str, parser):
117128
if not key in args:
118129
args[key] = val
119130

120-
for a, b in [("run", run ), ("test", test ), ("build", build),
121-
("clean", clean), ("bench", bench), ("count", count)]:
131+
for a, b in [("run", run ), ("test", test ), ("build", build),
132+
("clean", clean), ("bench", bench), ("count", count)]:
122133
append_defaults_to_data(a, b)
123134

124135
if args["command"] is None:
@@ -128,4 +139,11 @@ def append_defaults_to_data(name: str, parser):
128139
# "Slugify" the name of the job
129140
args["name"] = re.sub(r'[\W_]+', '-', args["name"])
130141

142+
for e in ["input", "input1", "input2"]:
143+
if e not in args:
144+
continue
145+
146+
if args[e] is not None:
147+
args[e] = os.path.abspath(args[e])
148+
131149
return args

toolchain/mfc/packer/__init__.py

Whitespace-only changes.

toolchain/mfc/packer/errors.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import dataclasses, math
2+
3+
@dataclasses.dataclass(repr=False)
4+
class Error:
5+
absolute: float
6+
relative: float
7+
8+
def __repr__(self) -> str:
9+
return f"abs: {self.absolute:.2E}, rel: {self.relative:.2E}"
10+
11+
12+
def compute_error(measured: float, expected: float) -> Error:
13+
absolute = abs(measured - expected)
14+
15+
if expected != 0:
16+
relative = absolute / abs(expected)
17+
elif measured == expected:
18+
relative = 0
19+
else:
20+
relative = float("NaN")
21+
22+
return Error(absolute, relative)
23+
24+
25+
class AverageError:
26+
accumulated: Error
27+
count: int
28+
29+
def __init__(self) -> None:
30+
self.accumulated = Error(0, 0)
31+
self.count = 0
32+
33+
def get(self) -> Error:
34+
if self.count == 0:
35+
return Error(0, 0)
36+
37+
return Error(self.accumulated.absolute / self.count,
38+
self.accumulated.relative / self.count)
39+
40+
def push(self, error: Error) -> None:
41+
# Do not include nans in the result
42+
# See: compute_error()
43+
if math.isnan(error.relative):
44+
return
45+
46+
self.accumulated.absolute += error.absolute
47+
self.accumulated.relative += error.relative
48+
49+
self.count += 1
50+
51+
def __repr__(self) -> str:
52+
return self.get().__repr__()

toolchain/mfc/packer/pack.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import dataclasses, typing, os, re, math
2+
3+
from .. import common
4+
from ..common import MFCException
5+
6+
from pathlib import Path
7+
8+
# This class maps to the data contained in one file in D/
9+
@dataclasses.dataclass(repr=False)
10+
class PackEntry:
11+
filepath: str
12+
doubles: typing.List[float]
13+
14+
def __repr__(self) -> str:
15+
return f"{self.filepath} {' '.join([ str(d) for d in self.doubles ])}"
16+
17+
18+
# This class maps to the data contained in the entirety of D/: it is tush a list
19+
# of PackEntry classes.
20+
class Pack:
21+
entries: typing.Dict[str, PackEntry]
22+
23+
def __init__(self):
24+
self.entries = {}
25+
26+
def __init__(self, entries: typing.List[PackEntry]):
27+
self.entries = {}
28+
for entry in entries:
29+
self.set(entry)
30+
31+
def find(self, filepath: str) -> PackEntry:
32+
return self.entries.get(filepath, None)
33+
34+
def set(self, entry: PackEntry):
35+
self.entries[entry.filepath] = entry
36+
37+
def save(self, filepath: str):
38+
if filepath.endswith(".py"):
39+
filepath = os.path.dirname(filepath)
40+
41+
if os.path.isdir(filepath):
42+
filepath = os.path.join(filepath, "pack.txt")
43+
44+
if not filepath.endswith(".txt"):
45+
filepath += ".txt"
46+
47+
common.file_write(filepath, '\n'.join([ str(e) for e in sorted(self.entries.values(), key=lambda x: x.filepath) ]))
48+
49+
def hash_NaNs(self) -> bool:
50+
for entry in self.entries.values():
51+
for double in entry.doubles:
52+
if math.isnan(double):
53+
return True
54+
55+
return False
56+
57+
58+
def load(filepath: str) -> Pack:
59+
if not os.path.isfile(filepath):
60+
filepath = os.path.join(filepath, "pack.txt")
61+
62+
entries: typing.List[PackEntry] = []
63+
64+
for line in common.file_read(filepath).splitlines():
65+
if common.isspace(line):
66+
continue
67+
68+
arr = line.split(' ')
69+
70+
entries.append(PackEntry(
71+
filepath=arr[0],
72+
doubles=[ float(d) for d in arr[1:] ]
73+
))
74+
75+
return Pack(entries)
76+
77+
78+
def compile(casepath: str) -> typing.Tuple[Pack, str]:
79+
entries = []
80+
81+
case_dir = os.path.dirname(casepath) if os.path.isfile(casepath) else casepath
82+
D_dir = os.path.join(case_dir, "D")
83+
84+
for filepath in list(Path(D_dir).rglob("*.dat")):
85+
short_filepath = str(filepath).replace(f'{case_dir}', '')[1:].replace("\\", "/")
86+
87+
try:
88+
doubles = [ float(e) for e in re.sub(r"[\n\t\s]+", " ", common.file_read(filepath)).strip().split(' ') ]
89+
except ValueError:
90+
None, f"Failed to interpret the content of [magenta]{filepath}[/magenta] as a list of floating point numbers."
91+
92+
entries.append(PackEntry(short_filepath,doubles))
93+
94+
return Pack(entries), None

toolchain/mfc/packer/packer.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import typing, os.path
2+
3+
from ..printer import cons
4+
from ..state import ARG, ARGS
5+
from . import pack as _pack
6+
from . import errors
7+
from . import tol as packtol
8+
from ..common import MFCException
9+
10+
def load(packpath: str) -> _pack.Pack:
11+
return _pack.load(packpath)
12+
13+
def pack(casepath: str, packpath: str = None) -> typing.Tuple[_pack.Pack, str]:
14+
if packpath is None:
15+
packpath = casepath
16+
17+
p, err = _pack.compile(casepath)
18+
19+
if err is not None:
20+
return None, err
21+
22+
p.save(packpath)
23+
24+
return p, None
25+
26+
def compare(lhs: str = None, rhs: str = None, tol: packtol.Tolerance = None) -> typing.Tuple[errors.Error, str]:
27+
if isinstance(lhs, str):
28+
lhs = load(lhs)
29+
if isinstance(rhs, str):
30+
rhs = load(rhs)
31+
32+
return packtol.compare(lhs, rhs, tol)
33+
34+
def packer():
35+
if ARG("packer") == "pack":
36+
if not os.path.isdir(ARG("input")):
37+
ARGS()["input"] = os.path.dirname(ARG("input"))
38+
39+
out_dir = os.path.sep.join([ARG("input"), ARG("output")]) if ARG("output") is not None else None
40+
p, err = pack(ARG("input"), out_dir)
41+
if err is not None:
42+
raise MFCException(err)
43+
elif ARG("packer") == "compare":
44+
cons.print(f"Comparing [magenta]{os.path.relpath(ARG('input1'), os.getcwd())}[/magenta] to [magenta]{os.path.relpath(ARG('input1'), os.getcwd())}[/magenta]:")
45+
46+
cons.indent()
47+
cons.print()
48+
49+
tol = packtol.Tolerance(ARG("abstol"), ARG("reltol"))
50+
51+
err, msg = compare(ARG("input1"), ARG("input2"), tol)
52+
if msg is not None:
53+
cons.print(f"[bold red]ERROR[/bold red]: The two packs are not within tolerance ({tol}).")
54+
cons.print(msg)
55+
else:
56+
cons.print(f"[bold green]OK[/bold green]: The two packs are within tolerance ({tol}).")
57+
cons.print(f"Average error: {err}.")
58+
59+
cons.print()
60+
cons.unindent()
61+
62+
else:
63+
raise MFCException(f"Unknown packer command: {ARG('packer')}")

toolchain/mfc/packer/tol.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import math, typing
2+
3+
from ..common import MFCException
4+
5+
from .pack import Pack
6+
from .errors import compute_error, AverageError, Error
7+
8+
class Tolerance(Error):
9+
pass
10+
11+
12+
def is_close(error: Error, tolerance: Tolerance) -> bool:
13+
if error.absolute <= tolerance.absolute:
14+
return True
15+
16+
if math.isnan(error.relative):
17+
return True
18+
19+
if error.relative <= tolerance.relative:
20+
return True
21+
22+
return False
23+
24+
25+
def compare(candidate: Pack, golden: Pack, tol: Tolerance) -> typing.Tuple[Error, str]:
26+
# Keep track of the average error
27+
avg_err = AverageError()
28+
29+
# Compare entry-count
30+
if len(candidate.entries) != len(golden.entries):
31+
return None, "Line count does not match."
32+
33+
# For every entry in the golden's pack
34+
for gFilepath, gEntry in golden.entries.items():
35+
# Find the corresponding entry in the candidate's pack
36+
cEntry = candidate.find(gFilepath)
37+
38+
if cEntry == None:
39+
return None, f"No reference to {gFilepath} in the candidate's pack."
40+
41+
# Compare variable-count
42+
if len(gEntry.doubles) != len(cEntry.doubles):
43+
return None, f"Variable count didn't match for {gFilepath}."
44+
45+
# Check if each variable is within tolerance
46+
for valIndex, (gVal, cVal) in enumerate(zip(gEntry.doubles, cEntry.doubles)):
47+
# Keep track of the error and average errors
48+
error = compute_error(cVal, gVal)
49+
avg_err.push(error)
50+
51+
def raise_err(msg: str):
52+
return None, f"""\
53+
Variable n°{valIndex+1} (1-indexed) in {gFilepath} {msg}:
54+
- Candidate: {cVal}
55+
- Golden: {gVal}
56+
- Error: {error}
57+
- Tolerance: {tol}
58+
"""
59+
60+
if math.isnan(gVal):
61+
raise_err("is NaN in the golden file")
62+
63+
if math.isnan(cVal):
64+
raise_err("is NaN in the pack file")
65+
66+
if not is_close(error, tol):
67+
raise_err("is not within tolerance")
68+
69+
# Return the average relative error
70+
return avg_err.get(), None

0 commit comments

Comments
 (0)