diff --git a/python/tamaas/__main__.py b/python/tamaas/__main__.py index bd56738..9e15870 100755 --- a/python/tamaas/__main__.py +++ b/python/tamaas/__main__.py @@ -1,250 +1,325 @@ #!/usr/bin/env python3 # -*- mode: python; coding: utf-8 -*- # vim: set ft=python: # # Copyright (©) 2016-2022 EPFL (École Polytechnique Fédérale de Lausanne), # Laboratory (LSMS - Laboratoire de Simulation en Mécanique des Solides) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published # by the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . """Module entry point.""" import sys import io import time import argparse import tamaas as tm import numpy as np +from types import SimpleNamespace +from pathlib import Path + __author__ = "Lucas Frérot" __copyright__ = ( "Copyright (©) 2019-2022, EPFL (École Polytechnique Fédérale de Lausanne)," "\nLaboratory (LSMS - Laboratoire de Simulation en Mécanique des Solides)" ) __license__ = "SPDX-License-Identifier: AGPL-3.0-or-later" def load_stream(stream): """ Load numpy from binary stream (allows piping). Code from https://gist.github.com/CMCDragonkai/3c99fd4aabc8278b9e17f50494fcc30a """ np_magic = stream.read(6) # use the sys.stdin.buffer to read binary data np_data = stream.read() # read it all into an io.BytesIO object return io.BytesIO(np_magic + np_data) def print_version(): """Print version information.""" print( f"""Tamaas {tm.__version__} {tm.__copyright__} Authors: {', '.join(tm.__author__)} This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.""" ) -def surface(args): +def make_surface(args): """Generate a surface.""" if args.generator == "random_phase": generator = tm.SurfaceGeneratorRandomPhase2D(args.sizes) elif args.generator == "filter": generator = tm.SurfaceGeneratorFilter2D(args.sizes) else: - raise ValueError("Unknown generator method {}".format(args.generator)) + raise ValueError(f"Unknown generator method {args.generator}") generator.spectrum = tm.Isopowerlaw2D() generator.spectrum.q0 = args.cutoffs[0] generator.spectrum.q1 = args.cutoffs[1] generator.spectrum.q2 = args.cutoffs[2] generator.spectrum.hurst = args.hurst - generator.random_seed = args.seed + + if 'seed' in args.__dict__: + generator.random_seed = args.seed surface = ( generator.buildSurface() / generator.spectrum.rmsSlopes() * args.rms ) - output = args.output if args.output is not None else sys.stdout - params = { "q0": generator.spectrum.q0, "q1": generator.spectrum.q1, "q2": generator.spectrum.q2, "hurst": generator.spectrum.hurst, "random_seed": generator.random_seed, - "rms_heights": args.rms, + "rms_slopes": args.rms, "generator": args.generator, } + return params, surface + + +def surface(args): + """Print a surface to stdout.""" + params, surface = make_surface(args) + + output = args.output if args.output is not None else sys.stdout + try: np.savetxt(output, surface, header=str(params)) except BrokenPipeError: pass +def declarative(args): + """Run simulation defined in YAML.""" + from ruamel.yaml import YAML + + tm.set_log_level(tm.LogLevel.error) + + if not args.input: + input = sys.stdin + else: + input = args.input + + yml = YAML(typ='safe') + data = yml.load(Path(input)) + + # Handle surface definition + if 'surface' not in data: + sys.exit("Error: simulation description does not declare a surface") + + if len(data['surface']) > 1: + sys.exit("Error: expected only one surface definition, got multiple") + + args = SimpleNamespace() + args.generator = list(data['surface'])[0] + + for k, v in data['surface'][args.generator].items(): + setattr(args, k, v) + + try: + _, surface = make_surface(args) + except AttributeError as e: + attr = str(e).split(" ")[-1].replace("'", "") + sys.exit(f"Error: Surface property '{attr}' required but not specified") + + # Handle model definition + if 'model' not in data: + sys.exit("Error: simulation description does not declare a model") + + mdata = data['model'] + + try: + if mdata['type'] not in {'basic_1d', 'basic_2d'}: + sys.exit(f'Error: unrecognized model type \'{mdata["type"]}\'') + + mtype = getattr(tm.model_type, mdata['type']) + model = tm.ModelFactory.createModel( + mtype, mdata['system_size'], mdata['shape'], + ) + + except KeyError as e: + sys.exit(f"Error: model property {e} required but not specified") + + model.E = mdata.get('E', 1) + model.nu = mdata.get('nu', 0) + + def contact(args): """Solve a contact problem.""" from tamaas.dumpers import NumpyDumper tm.set_log_level(tm.LogLevel.error) if not args.input: input = sys.stdin else: input = args.input surface = np.loadtxt(input) discretization = surface.shape system_size = [1.0, 1.0] model = tm.ModelFactory.createModel( tm.model_type.basic_2d, system_size, discretization ) solver = tm.PolonskyKeerRey(model, surface, args.tol) solver.solve(args.load) dumper = NumpyDumper("numpy", "traction", "displacement") dumper.dump_to_file(sys.stdout.buffer, model) def plot(args): """Plot displacement and pressure maps.""" try: import matplotlib.pyplot as plt except ImportError: print( "Please install matplotlib to use the 'plot' command", file=sys.stderr, ) sys.exit(1) fig, (ax_traction, ax_displacement) = plt.subplots(1, 2) ax_traction.set_title("Traction") ax_displacement.set_title("Displacement") with load_stream(sys.stdin.buffer) as f_np: data = np.load(f_np) ax_traction.imshow(data["traction"]) ax_displacement.imshow(data["displacement"]) fig.set_size_inches(10, 6) fig.tight_layout() plt.show() def main(): - """Main function.""" + """Process command line arguments.""" parser = argparse.ArgumentParser( prog="tamaas", description=( "The tamaas command is a simple utility for surface" " generation, contact computation and" " plotting of contact solutions" ), ) parser.add_argument( "--version", action="store_true", help="print version info" ) subs = parser.add_subparsers( title="commands", description="utility commands" ) # Arguments for surface command parser_surface = subs.add_parser( "surface", description="Generate a self-affine rough surface" ) parser_surface.add_argument( "--cutoffs", "-K", nargs=3, type=int, help="Long, rolloff, short wavelength cutoffs", metavar=("k_l", "k_r", "k_s"), required=True, ) parser_surface.add_argument( "--sizes", nargs=2, type=int, help="Number of points", metavar=("nx", "ny"), required=True, ) parser_surface.add_argument( "--hurst", "-H", type=float, help="Hurst exponent", required=True ) parser_surface.add_argument( "--rms", type=float, help="Root-mean-square of slopes", default=1.0 ) parser_surface.add_argument( "--seed", type=int, help="Random seed", default=int(time.time()) ) parser_surface.add_argument( "--generator", help="Generation method", choices=("random_phase", "filter"), default="random_phase", ) parser_surface.add_argument( "--output", "-o", help="Output file name (compressed if .gz)" ) parser_surface.set_defaults(func=surface) # Arguments for contact command parser_contact = subs.add_parser( "contact", description="Compute the elastic contact solution with a given surface", ) parser_contact.add_argument( "--input", "-i", help="Rough surface file (default stdin)" ) parser_contact.add_argument( "--tol", type=float, default=1e-12, help="Solver tolerance" ) parser_contact.add_argument( "load", type=float, help="Applied average pressure" ) parser_contact.set_defaults(func=contact) # Arguments for plot command parser_plot = subs.add_parser("plot", description="Plot contact solution") parser_plot.set_defaults(func=plot) + # Arguments for declarative command + parser_decl = subs.add_parser( + "declarative", + description="Run simulation described by YAML file" + ) + parser_decl.add_argument("input", help="YAML file") + parser_decl.set_defaults(func=declarative) + args = parser.parse_args() if args.version: print_version() return try: args.func(args) except AttributeError: parser.print_usage() if __name__ == "__main__": main()