diff --git a/.nextmv/readme/python-hexaly-generic/1.sh b/.nextmv/readme/python-hexaly-generic/1.sh index b3c4c49..6c2e312 100644 --- a/.nextmv/readme/python-hexaly-generic/1.sh +++ b/.nextmv/readme/python-hexaly-generic/1.sh @@ -1 +1 @@ -python3 main.py -duration 30 +python3 main.py inFileName=inputs/input.dat solFileName=outputs/solutions/output.txt diff --git a/.nextmv/readme/python-hexaly-generic/2.sh b/.nextmv/readme/python-hexaly-generic/2.sh index 13da292..9a59ab3 100644 --- a/.nextmv/readme/python-hexaly-generic/2.sh +++ b/.nextmv/readme/python-hexaly-generic/2.sh @@ -1 +1,4 @@ -nextmv push --app-id +python3 main.py \ + inFileName=input.dat \ + solFileName=outputs/solutions/output.txt \ + unNest=true diff --git a/.nextmv/readme/python-hexaly-generic/3.sh b/.nextmv/readme/python-hexaly-generic/3.sh index f062c46..13da292 100644 --- a/.nextmv/readme/python-hexaly-generic/3.sh +++ b/.nextmv/readme/python-hexaly-generic/3.sh @@ -1,3 +1 @@ -docker run -i --rm \ --v $(pwd):/app ghcr.io/nextmv-io/runtime/hexaly:latest \ -sh -c 'python3 /app/main.py' +nextmv push --app-id diff --git a/.nextmv/readme/python-hexaly-generic/4.sh b/.nextmv/readme/python-hexaly-generic/4.sh new file mode 100644 index 0000000..c102959 --- /dev/null +++ b/.nextmv/readme/python-hexaly-generic/4.sh @@ -0,0 +1,5 @@ +nextmv app run --app-id \ + --input inputs/ \ + --content-type multi-file \ + --secret-collection-id \ + --options 'inFileName=inputs/input.dat,solFileName=outputs/solutions/output.txt' diff --git a/.nextmv/readme/python-hexaly-generic/5.sh b/.nextmv/readme/python-hexaly-generic/5.sh new file mode 100644 index 0000000..f062c46 --- /dev/null +++ b/.nextmv/readme/python-hexaly-generic/5.sh @@ -0,0 +1,3 @@ +docker run -i --rm \ +-v $(pwd):/app ghcr.io/nextmv-io/runtime/hexaly:latest \ +sh -c 'python3 /app/main.py' diff --git a/.nextmv/readme/workflow-configuration.yml b/.nextmv/readme/workflow-configuration.yml index d739455..441fddc 100644 --- a/.nextmv/readme/workflow-configuration.yml +++ b/.nextmv/readme/workflow-configuration.yml @@ -132,6 +132,10 @@ apps: skip: true - name: 3.sh skip: true + - name: 4.sh + skip: true + - name: 5.sh + skip: true - name: python-highs-knapsack scripts: - name: 0.sh diff --git a/python-hexaly-generic/.gitignore b/python-hexaly-generic/.gitignore new file mode 100644 index 0000000..4b1638f --- /dev/null +++ b/python-hexaly-generic/.gitignore @@ -0,0 +1,2 @@ +model.hxm +input.dat diff --git a/python-hexaly-generic/README.md b/python-hexaly-generic/README.md index 18b026a..2e2398e 100644 --- a/python-hexaly-generic/README.md +++ b/python-hexaly-generic/README.md @@ -20,19 +20,32 @@ multi knapsack Mixed Integer Programming problem. pip3 install -r requirements.txt ``` -1. Put your model file and input data in the `inputs/` directory. The model file - should have the extension `.hxm` and the input data file should have the - extension `.dat`. See the example files in the `inputs/` directory for - reference. +1. Put your model file and any other necessary files in the `inputs/` directory. + The _model file_ should have the extension `.hxm`. All other files need to be + either referenced by your model code or specified as input arguments via + options (e.g., `-data=`). See the example files in the `inputs/` + directory for reference. - The model automatically loads the first `.hxm` file (alternatively, the - first `.lsp` file) and the first `.dat` file it finds in the input - directory. Use the `-model` and `-data` flags to specify specific files. -1. Run the app. + first `.lsp` file) it finds in the input directory. +1. Run the app locally. ```bash - python3 main.py -duration 30 + python3 main.py inFileName=inputs/input.dat solFileName=outputs/solutions/output.txt ``` + - If your app expects inputs files to be in the same directory as the model + file, you can use the `unNest=true` option. The app will then copy all + files from the `inputs/` directory to the current working directory before + running the model. Even though it is not necessary for this example, you + can test this by running (note the path to the data file): + + ```bash + python3 main.py \ + inFileName=input.dat \ + solFileName=outputs/solutions/output.txt \ + unNest=true + ``` + 1. If above steps were successful, you can push the app to the Nextmv Platform. E.g., using the [Nextmv CLI][install-cli]: @@ -40,6 +53,20 @@ multi knapsack Mixed Integer Programming problem. nextmv push --app-id ``` +1. You can then run the app on the Nextmv Platform by using the CLI (note that + you need to have the license file defined as a [secret][secret] in your + Nextmv Application): + + ```bash + nextmv app run --app-id \ + --input inputs/ \ + --content-type multi-file \ + --secret-collection-id \ + --options 'inFileName=inputs/input.dat,solFileName=outputs/solutions/output.txt' + ``` + + Or you can run it via the [Nextmv Console][console]. + ## Mirror running on Nextmv Cloud locally Docker needs to be installed. diff --git a/python-hexaly-generic/main.py b/python-hexaly-generic/main.py index e95c755..a4e2c46 100644 --- a/python-hexaly-generic/main.py +++ b/python-hexaly-generic/main.py @@ -1,52 +1,90 @@ import os +import shutil +import sys import nextmv from hexaly.modeler import HexalyModeler +# Name of the option that makes the app copy all files from the `inputs/` directory to the +# current working directory before running the model. +OPTION_UN_NEST = "unNest" + def main() -> None: - options = nextmv.Options( - nextmv.Option("input", str, "inputs/", "input path", False), - nextmv.Option("model", str, "", "model file path", False), - nextmv.Option("data", str, "", "data file path", False), - nextmv.Option("output", str, "outputs/solutions/", "output path", False), - nextmv.Option("duration", int, 30, "max runtime in seconds", False), - ) - - os.makedirs(options.output, exist_ok=True) - - # Determine model and data files. - if options.model: - model_path = os.path.join(options.input, options.model) - else: - model_path = find_file(options.input, [".hxm", ".lsp"]) - if options.data: - data_path = os.path.join(options.input, options.data) - else: - data_path = find_file(options.input, [".dat"]) - nextmv.log(f"Using model file: {model_path}") - nextmv.log(f"Using data file: {data_path}") + """Entry point for the program.""" + + # Parse options from command line arguments. + options, un_nest = parse_options() + nextmv.log("Options:") + for key, value in options.items(): + nextmv.log(f" - {key}: {value}") + + # Make sure the output directory exists. + os.makedirs(os.path.join("outputs", "solutions"), exist_ok=True) + + # If the `unNest=true` option is set, copy all files from the `inputs/` directory to + # the current working directory. + if un_nest: + nextmv.log("Using unNest option, copying files from inputs/ to current directory.") + unnest_directory("inputs") + + # Find the model file in the specified path. + model_path = find_file("inputs", [".hxm", ".lsp"]) + nextmv.log(f"Model file found: {model_path}") + + # Prepare options for consumption by the model. + options_list = [f"{key}={value}" for key, value in options.items()] # Load and solve the model. + nextmv.log("Loading and solving the model...") with HexalyModeler() as modeler: optimizer = modeler.create_optimizer() module = modeler.load_module("model", model_path) module.run( optimizer, - f"inFileName={data_path}", - f"solFileName={options.output}/output.txt", - f"hxTimeLimit={options.duration}", + *options_list, ) - with open(f"{options.output}/output.txt") as f: - nextmv.write( - nextmv.Output( - solution=f.read(), - options=options.to_dict(), - output_format=nextmv.OutputFormat.MULTI_FILE, - ), - path=options.output, - ) + nextmv.log("Done.") + + +def parse_options() -> tuple[dict[str, str], bool]: + """ + Parses all arguments so that they can be submitted to the model. Returns a dictionary + of options and a boolean indicating whether the inputs directory should be un-nested. + """ + un_nest = False + options = {} + for arg in sys.argv[1:]: + if arg.startswith("--"): + arg = arg[2:] + elif arg.startswith("-"): + arg = arg[1:] + if arg == OPTION_UN_NEST: + un_nest = True + continue + if "=" in arg: + key, value = arg.split("=", 1) + if key == OPTION_UN_NEST: + un_nest = True + continue + options[key] = value + else: + options[arg] = "true" + return options, un_nest + + +def unnest_directory(source_directory: str) -> None: + """ + Copies all files from the source directory to the current working directory. + """ + # Iterate over all the items in the source directory + for root, _, files in os.walk(source_directory): + for file in files: + # Construct the full file path + source_file_path = os.path.join(root, file) + # Copy the file to the current directory + shutil.copy2(source_file_path, ".") def find_file(path: str, extensions: list[str]) -> str: