Skip to content

Commit

Permalink
Add logging to CLI
Browse files Browse the repository at this point in the history
  • Loading branch information
dhadka committed Sep 20, 2024
1 parent eed5cc9 commit 36541ec
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 79 deletions.
25 changes: 12 additions & 13 deletions docs/cli.rst
Original file line number Diff line number Diff line change
Expand Up @@ -47,21 +47,10 @@ which outputs:

0.23519328623761482

Plotting
--------

For 2 and 3 objectives, we can also generate a plot, either interactive or saving as an image:

.. code:: bash
python -m platypus plot NSGAII_DTLZ2.set
python -m platypus plot --output NSGAII_DTLZ2.png NSGAII_DTLZ2.set
Filtering
---------

The results can be filtered to remove any dominated solutions, with optional support to remove
infeasible, duplicate, or using epsilon-dominance:
The results can be filtered to remove any dominated, infeasible, or duplicate solutions:

.. code:: bash
Expand All @@ -81,6 +70,16 @@ the normalized solutions:
python -m platypus normalize --output NSGAII_DTLZ2_normalized.set --reference_set ./examples/DTLZ2.2D.pf NSGAII_DTLZ2.set
python -m platypus normalize --output NSGAII_DTLZ2_normalized.set --minimum 0.0,0.0 --maximum 1.0,1.0 NSGAII_DTLZ2.set
Plotting
--------

For 2 and 3 objectives, we can also generate a plot, either interactive or saving as an image:

.. code:: bash
python -m platypus plot NSGAII_DTLZ2.set
python -m platypus plot --output NSGAII_DTLZ2.png NSGAII_DTLZ2.set
Combining or Chaining Commands
------------------------------

Expand All @@ -91,5 +90,5 @@ as demonstrated below:
.. code:: bash
python -m platypus solve --algorithm NSGAII --problem DTLZ2 --nfe 10000 | \
python -m platypus filter -e 0.01,0.01 | \
python -m platypus filter --epsilons 0.01,0.01 | \
python -m platypus plot
92 changes: 62 additions & 30 deletions platypus/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@
#
# You should have received a copy of the GNU General Public License
# along with Platypus. If not, see <http://www.gnu.org/licenses/>.

import os
import re
import sys
import json
import locale
import random
import logging
import platypus
Expand All @@ -27,40 +29,80 @@
coalesce

def main(input):
logging.basicConfig(level=logging.INFO)
"""The main entry point for the Platypus CLI."""
LOGGER = logging.getLogger("Platypus")

def load_set(file):
"""Loads input file from stdin or file."""
if file is None:
return platypus.load(sys.stdin)

try:
return platypus.load_json(file)
except json.decoder.JSONDecodeError:
return platypus.load_objectives(file)

def save_set(result, file, indent=4):
"""Output result to stdout or file."""
if file is None:
platypus.dump(result, sys.stdout, indent=indent)
sys.stdout.write(os.linesep)
else:
platypus.save_json(file, result, indent=indent)

def split_list(type, separator=None):
"""Argparse type for comma-separated list of values.
Accepts arguments of the form::
--arg val1,val2,val3
This is, in my opinion, a bit easier to use than argparse's default
which will capture any subsequent positional arguments unless
separated by ``--``.
By default, supports either ``,`` or ``;`` as the separator, except
in locales using that character as a decimal point.
"""
separator = coalesce(separator, "".join({",", ";"}.difference(locale.localeconv()["decimal_point"])))
pattern = re.compile(f"[{re.escape(separator)}]")
return lambda input: [type(s.strip()) for s in pattern.split(input)]

def debug_inputs(args):
"""Log CLI arguments."""
for attr in dir(args):
if not attr.startswith("_"):
LOGGER.debug("Argument: %s=%s", attr, getattr(args, attr))

parser = ArgumentParser(prog="platypus",
description="Platypus (platypus-opt) - Multobjective optimization in Python")

parser.add_argument("-v", "--version", action="version", version=platypus.__version__)
parser.add_argument('--log', help='set the logging level', type=str.upper, default='WARNING',
choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"])

subparsers = parser.add_subparsers(title="commands", required=True, dest="command")

# Use comma-separated lists instead of nargs, since nargs="+" or nargs="*" will capture positional
# arguments unless separated by --.
def split_list(type):
return lambda input: [type(s.strip()) for s in input.split(',')]

hypervolume_parser = subparsers.add_parser("hypervolume", help="compute hypervolume")
hypervolume_parser.add_argument("-r", "--reference_set", help="reference set")
hypervolume_parser.add_argument("--minimum", help="minimum bounds, optional", type=split_list(float))
hypervolume_parser.add_argument("--maximum", help="maximum bounds, optional", type=split_list(float))
hypervolume_parser.add_argument("filename", nargs="?")
hypervolume_parser.add_argument("filename", help="input filename", nargs="?")

gd_parser = subparsers.add_parser("gd", help="compute generaional distance")
gd_parser.add_argument("-r", "--reference_set", help="reference set", required=True)
gd_parser.add_argument("filename", nargs="?")
gd_parser.add_argument("filename", help="input filename", nargs="?")

igd_parser = subparsers.add_parser("igd", help="compute inverted generaional distance")
igd_parser.add_argument("-r", "--reference_set", help="reference set", required=True)
igd_parser.add_argument("filename", nargs="?")
igd_parser.add_argument("filename", help="input filename", nargs="?")

epsilon_parser = subparsers.add_parser("epsilon", help="compute additive epsilon indicator")
epsilon_parser.add_argument("-r", "--reference_set", help="reference set", required=True)
epsilon_parser.add_argument("filename", nargs="?")
epsilon_parser.add_argument("filename", help="input filename", nargs="?")

spacing_parser = subparsers.add_parser("spacing", help="compute spacing")
spacing_parser.add_argument("filename", nargs="?")
spacing_parser.add_argument("filename", help="input filename", nargs="?")

solve_parser = subparsers.add_parser("solve", help="solve a built-in problem")
solve_parser.add_argument("-p", "--problem", help="name of the problem", required=True)
Expand All @@ -78,36 +120,26 @@ def split_list(type):
filter_parser.add_argument("-u", "--unique", help="remove any duplicate solutions", action='store_true')
filter_parser.add_argument("-n", "--nondominated", help="remove any dominated solutions", action='store_true')
filter_parser.add_argument("-o", "--output", help="output filename")
filter_parser.add_argument("filename", nargs="?")
filter_parser.add_argument("filename", help="input filename", nargs="?")

normalize_parser = subparsers.add_parser("normalize", help="normalize results")
normalize_parser.add_argument("-r", "--reference_set", help="reference set")
normalize_parser.add_argument("--minimum", help="minimum values for each objective", type=split_list(float))
normalize_parser.add_argument("--maximum", help="maximum values for each objective", type=split_list(float))
normalize_parser.add_argument("-o", "--output", help="output filename")
normalize_parser.add_argument("filename", nargs="?")
normalize_parser.add_argument("filename", help="input filename", nargs="?")

plot_parser = subparsers.add_parser("plot", help="generate simple 2D or 3D plot")
plot_parser.add_argument("-t", "--title", help="plot title")
plot_parser.add_argument("-o", "--output", help="output filename")
plot_parser.add_argument("filename", nargs="?")
plot_parser.add_argument("filename", help="input filename", nargs="?")

args = parser.parse_args(input)

def load_set(file):
if file is None:
return platypus.load(sys.stdin)

try:
return platypus.load_json(file)
except json.decoder.JSONDecodeError:
return platypus.load_objectives(file)
if args.log:
logging.basicConfig(level=args.log)

def save_set(result, file, indent=4):
if file is None:
platypus.dump(result, sys.stdout, indent=indent)
else:
platypus.save_json(file, result, indent=indent)
debug_inputs(args)

if args.command == "hypervolume":
ref_set = load_set(args.reference_set)
Expand Down Expand Up @@ -185,12 +217,12 @@ def save_set(result, file, indent=4):

if args.reference_set:
if minimum is not None or maximum is not None:
print("ignoring --minimum and --maximum options since a reference set is provided", file=sys.stderr)
LOGGER.warn("ignoring --minimum and --maximum options since a reference set is provided", file=sys.stderr)
ref_set = load_set(args.reference_set)
minimum, maximum = platypus.normalize(ref_set)

norm_min, norm_max = platypus.normalize(input_set, minimum, maximum)
print(f"Using bounds minimum={norm_min}; maximum={norm_max}")
LOGGER.info(f"Using bounds minimum={norm_min}; maximum={norm_max}")

for s in input_set:
s.objectives = s.normalized_objectives
Expand Down
2 changes: 1 addition & 1 deletion platypus/_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ def only_keys_for(d, func):
def log_args(args, target):
"""Logs the arguments."""
for k, v in args.items():
LOGGER.log(logging.INFO, "Setting %s=%s on %s", k, v, target)
LOGGER.info("Setting %s=%s on %s", k, v, target)

def parse_cli_keyvalue(args):
"""Parses CLI key=value pairs into a dictionary.
Expand Down
20 changes: 9 additions & 11 deletions platypus/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -516,26 +516,24 @@ def run(self, condition, callback=None):
last_log = self.nfe
start_time = time.time()

LOGGER.log(logging.INFO, "%s starting", type(self).__name__)
LOGGER.info("%s starting", type(self).__name__)

while not condition(self):
self.step()

if self.log_frequency is not None and self.nfe >= last_log + self.log_frequency:
LOGGER.log(logging.INFO,
"%s running; NFE Complete: %d, Elapsed Time: %s",
type(self).__name__,
self.nfe,
datetime.timedelta(seconds=time.time()-start_time))
LOGGER.info("%s running; NFE Complete: %d, Elapsed Time: %s",
type(self).__name__,
self.nfe,
datetime.timedelta(seconds=time.time()-start_time))

if callback is not None:
callback(self)

LOGGER.log(logging.INFO,
"%s finished; Total NFE: %d, Elapsed Time: %s",
type(self).__name__,
self.nfe,
datetime.timedelta(seconds=time.time()-start_time))
LOGGER.info("%s finished; Total NFE: %d, Elapsed Time: %s",
type(self).__name__,
self.nfe,
datetime.timedelta(seconds=time.time()-start_time))

def _constraint_eq(x, y):
return abs(x - y)
Expand Down
45 changes: 21 additions & 24 deletions platypus/evaluator.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,11 +142,10 @@ def evaluate_all(self, jobs, **kwargs):

for chunk in _chunks(jobs, log_frequency):
result.extend(self.map_func(run_job, chunk))
LOGGER.log(logging.INFO,
"%s running; Jobs Complete: %d, Elapsed Time: %s",
job_name,
len(result),
datetime.timedelta(seconds=time.time()-start_time))
LOGGER.info("%s running; Jobs Complete: %d, Elapsed Time: %s",
job_name,
len(result),
datetime.timedelta(seconds=time.time()-start_time))

return result

Expand Down Expand Up @@ -192,11 +191,10 @@ def evaluate_all(self, jobs, **kwargs):

for chunk in _chunks(futures, log_frequency):
result.extend([f.result() for f in chunk])
LOGGER.log(logging.INFO,
"%s running; Jobs Complete: %d, Elapsed Time: %s",
job_name,
len(result),
datetime.timedelta(seconds=time.time()-start_time))
LOGGER.info("%s running; Jobs Complete: %d, Elapsed Time: %s",
job_name,
len(result),
datetime.timedelta(seconds=time.time()-start_time))

return result

Expand Down Expand Up @@ -246,11 +244,10 @@ def evaluate_all(self, jobs, **kwargs):

for chunk in _chunks(futures, log_frequency):
result.extend([f.get() for f in chunk])
LOGGER.log(logging.INFO,
"%s running; Jobs Complete: %d, Elapsed Time: %s",
job_name,
len(result),
datetime.timedelta(seconds=time.time()-start_time))
LOGGER.info("%s running; Jobs Complete: %d, Elapsed Time: %s",
job_name,
len(result),
datetime.timedelta(seconds=time.time()-start_time))

return result

Expand Down Expand Up @@ -279,19 +276,19 @@ def __init__(self, pool):
self.pool = pool

if hasattr(pool, "_processes"):
LOGGER.log(logging.INFO, "Started pool evaluator with %d processes", pool._processes)
LOGGER.info("Started pool evaluator with %d processes", pool._processes)
else:
LOGGER.log(logging.INFO, "Started pool evaluator")
LOGGER.info("Started pool evaluator")

def close(self):
LOGGER.log(logging.DEBUG, "Closing pool evaluator")
LOGGER.debug("Closing pool evaluator")
self.pool.close()

if hasattr(self.pool, "join"):
LOGGER.log(logging.DEBUG, "Waiting for all processes to complete")
LOGGER.debug("Waiting for all processes to complete")
self.pool.join()

LOGGER.log(logging.INFO, "Closed pool evaluator")
LOGGER.info("Closed pool evaluator")

class MultiprocessingEvaluator(PoolEvaluator):
"""Evaluator using Python's multiprocessing library.
Expand Down Expand Up @@ -333,15 +330,15 @@ def __init__(self, processes=None):
from concurrent.futures import ProcessPoolExecutor
self.executor = ProcessPoolExecutor(processes)
super().__init__(self.executor.submit)
LOGGER.log(logging.INFO, "Started process pool evaluator")
LOGGER.info("Started process pool evaluator")

if processes:
LOGGER.log(logging.INFO, "Using user-defined number of processes: %d", processes)
LOGGER.info("Using user-defined number of processes: %d", processes)
except ImportError:
# prevent error from showing in Eclipse if concurrent.futures not available
raise

def close(self):
LOGGER.log(logging.DEBUG, "Closing process pool evaluator")
LOGGER.debug("Closing process pool evaluator")
self.executor.shutdown()
LOGGER.log(logging.INFO, "Closed process pool evaluator")
LOGGER.info("Closed process pool evaluator")

0 comments on commit 36541ec

Please sign in to comment.