Repository: stan-dev/cmdstanpy

Issue:
Optimize fail to parse the tol_grad argument
Hi

I was trying to use `optimize` to acquire the map estimation of a model but when I set the tol_grad parameter:

```python
 map_estimate = model.optimize(
        data=args,
        algorithm='lbfgs',
        tol_grad=1e-20
    )
```


 a RuntimeError got raised:

```
tol_grad=1e-20 is either mistyped or misplaced.
Perhaps you meant one of the following valid configurations?
  method=optimize optimize algorithm=bfgs bfgs tol_grad=<double>
  method=optimize optimize algorithm=lbfgs lbfgs tol_grad=<double>
```

Comments:

Patch:
diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
index f386381..a064ea2 100644
--- a/.github/ISSUE_TEMPLATE.md
+++ b/.github/ISSUE_TEMPLATE.md
@@ -10,5 +10,5 @@ Describe the issue as clearly as possible.
 Provide any additional information here.
 
 #### Current Version:
-Please include the output of `cmdstanpy.show_versions()`, or
+Please include the output of `import cmdstanpy; cmdstanpy.show_versions()`, or
 at least the cmdstan and cmdstanpy versions used.
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 23f8522..5812b91 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -60,7 +60,7 @@ jobs:
           pip install codecov
 
       - name: Run flake8, pylint, mypy
-        if: matrix.python-version == '3.10'
+        if: matrix.python-version == '3.10.6'
         run: |
           flake8 cmdstanpy test
           pylint -v cmdstanpy test
diff --git a/cmdstanpy/cmdstan_args.py b/cmdstanpy/cmdstan_args.py
index 9bbec6c..2fc7526 100644
--- a/cmdstanpy/cmdstan_args.py
+++ b/cmdstanpy/cmdstan_args.py
@@ -396,7 +396,7 @@ class OptimizeArgs:
         history_size: Optional[int] = None,
     ) -> None:
 
-        self.algorithm = algorithm
+        self.algorithm = algorithm or ""
         self.init_alpha = init_alpha
         self.iter = iter
         self.save_iterations = save_iterations
@@ -414,10 +414,7 @@ class OptimizeArgs:
         """
         Check arguments correctness and consistency.
         """
-        if (
-            self.algorithm is not None
-            and self.algorithm not in self.OPTIMIZE_ALGOS
-        ):
+        if self.algorithm and self.algorithm not in self.OPTIMIZE_ALGOS:
             raise ValueError(
                 'Please specify optimizer algorithms as one of [{}]'.format(
                     ', '.join(self.OPTIMIZE_ALGOS)
@@ -425,9 +422,9 @@ class OptimizeArgs:
             )
 
         if self.init_alpha is not None:
-            if self.algorithm == 'Newton':
+            if self.algorithm.lower() not in {'lbfgs', 'bfgs'}:
                 raise ValueError(
-                    'init_alpha must not be set when algorithm is Newton'
+                    'init_alpha requires that algorithm be set to bfgs or lbfgs'
                 )
             if isinstance(self.init_alpha, float):
                 if self.init_alpha <= 0:
@@ -443,9 +440,9 @@ class OptimizeArgs:
                 raise ValueError('iter must be type of int')
 
         if self.tol_obj is not None:
-            if self.algorithm == 'Newton':
+            if self.algorithm.lower() not in {'lbfgs', 'bfgs'}:
                 raise ValueError(
-                    'tol_obj must not be set when algorithm is Newton'
+                    'tol_obj requires that algorithm be set to bfgs or lbfgs'
                 )
             if isinstance(self.tol_obj, float):
                 if self.tol_obj <= 0:
@@ -454,9 +451,10 @@ class OptimizeArgs:
                 raise ValueError('tol_obj must be type of float')
 
         if self.tol_rel_obj is not None:
-            if self.algorithm == 'Newton':
+            if self.algorithm.lower() not in {'lbfgs', 'bfgs'}:
                 raise ValueError(
-                    'tol_rel_obj must not be set when algorithm is Newton'
+                    'tol_rel_obj requires that algorithm be set to bfgs'
+                    ' or lbfgs'
                 )
             if isinstance(self.tol_rel_obj, float):
                 if self.tol_rel_obj <= 0:
@@ -465,9 +463,9 @@ class OptimizeArgs:
                 raise ValueError('tol_rel_obj must be type of float')
 
         if self.tol_grad is not None:
-            if self.algorithm == 'Newton':
+            if self.algorithm.lower() not in {'lbfgs', 'bfgs'}:
                 raise ValueError(
-                    'tol_grad must not be set when algorithm is Newton'
+                    'tol_grad requires that algorithm be set to bfgs or lbfgs'
                 )
             if isinstance(self.tol_grad, float):
                 if self.tol_grad <= 0:
@@ -476,9 +474,10 @@ class OptimizeArgs:
                 raise ValueError('tol_grad must be type of float')
 
         if self.tol_rel_grad is not None:
-            if self.algorithm == 'Newton':
+            if self.algorithm.lower() not in {'lbfgs', 'bfgs'}:
                 raise ValueError(
-                    'tol_rel_grad must not be set when algorithm is Newton'
+                    'tol_rel_grad requires that algorithm be set to bfgs'
+                    ' or lbfgs'
                 )
             if isinstance(self.tol_rel_grad, float):
                 if self.tol_rel_grad <= 0:
@@ -487,9 +486,9 @@ class OptimizeArgs:
                 raise ValueError('tol_rel_grad must be type of float')
 
         if self.tol_param is not None:
-            if self.algorithm == 'Newton':
+            if self.algorithm.lower() not in {'lbfgs', 'bfgs'}:
                 raise ValueError(
-                    'tol_param must not be set when algorithm is Newton'
+                    'tol_param requires that algorithm be set to bfgs or lbfgs'
                 )
             if isinstance(self.tol_param, float):
                 if self.tol_param <= 0:
@@ -498,10 +497,9 @@ class OptimizeArgs:
                 raise ValueError('tol_param must be type of float')
 
         if self.history_size is not None:
-            if self.algorithm == 'Newton' or self.algorithm == 'BFGS':
+            if self.algorithm.lower() != 'lbfgs':
                 raise ValueError(
-                    'history_size must not be set when algorithm is '
-                    'Newton or BFGS'
+                    'history_size requires that algorithm be set to lbfgs'
                 )
             if isinstance(self.history_size, int):
                 if self.history_size < 0:
diff --git a/cmdstanpy/model.py b/cmdstanpy/model.py
index 723bc7e..81e8464 100644
--- a/cmdstanpy/model.py
+++ b/cmdstanpy/model.py
@@ -210,12 +210,6 @@ class CmdStanModel:
 
         if compile and self._exe_file is None:
             self.compile(force=str(compile).lower() == 'force')
-            if self._exe_file is None:
-                raise ValueError(
-                    'Unable to compile Stan model file: {}.'.format(
-                        self._stan_file
-                    )
-                )
 
     def __repr__(self) -> str:
         repr = 'CmdStanModel: name={}'.format(self._name)
@@ -531,45 +525,26 @@ class CmdStanModel:
                 get_logger().info(
                     'compiled model executable: %s', self._exe_file
                 )
-            if compilation_failed or 'Warning' in console:
+            if 'Warning' in console:
                 lines = console.split('\n')
                 warnings = [x for x in lines if x.startswith('Warning')]
-                syntax_errors = [
-                    x for x in lines if x.startswith('Syntax error')
-                ]
-                semantic_errors = [
-                    x for x in lines if x.startswith('Semantic error')
-                ]
-                exceptions = [
-                    x
-                    for x in lines
-                    if 'Uncaught exception' in x or 'fatal error' in x
-                ]
-                if (
-                    len(syntax_errors) > 0
-                    or len(semantic_errors) > 0
-                    or len(exceptions) > 0
-                ):
-                    get_logger().error('Stan program failed to compile:')
-                    get_logger().warning(console)
-                elif len(warnings) > 0:
-                    get_logger().warning(
-                        'Stan compiler has produced %d warnings:',
-                        len(warnings),
-                    )
-                    get_logger().warning(console)
-                elif (
-                    'PCH file' in console
-                    or 'model_header.hpp.gch' in console
-                    or 'precompiled header' in console
-                ):
+                get_logger().warning(
+                    'Stan compiler has produced %d warnings:',
+                    len(warnings),
+                )
+                get_logger().warning(console)
+            if compilation_failed:
+                if 'PCH' in console or 'precompiled header' in console:
                     get_logger().warning(
                         "CmdStan's precompiled header (PCH) files "
                         "may need to be rebuilt."
-                        "If your model failed to compile please run "
-                        "cmdstanpy.rebuild_cmdstan().\nIf the "
-                        "issue persists please open a bug report"
+                        "Please run cmdstanpy.rebuild_cmdstan().\n"
+                        "If the issue persists please open a bug report"
                     )
+                raise ValueError(
+                    f"Failed to compile Stan model '{self._stan_file}'. "
+                    f"Console:\n{console}"
+                )
 
     def optimize(
         self,
@@ -726,7 +701,9 @@ class CmdStanModel:
             self._run_cmdstan(runset, dummy_chain_id, show_console=show_console)
 
         if not runset._check_retcodes():
-            msg = 'Error during optimization: {}'.format(runset.get_err_msgs())
+            msg = "Error during optimization! Command '{}' failed: {}".format(
+                ' '.join(runset.cmd(0)), runset.get_err_msgs()
+            )
             if 'Line search failed' in msg and not require_converged:
                 get_logger().warning(msg)
             else:


Test patch:
diff --git a/test/test_cmdstan_args.py b/test/test_cmdstan_args.py
index ea2c4c2..39cc5ce 100644
--- a/test/test_cmdstan_args.py
+++ b/test/test_cmdstan_args.py
@@ -34,11 +34,14 @@ class OptimizeArgsTest(unittest.TestCase):
         self.assertIn('algorithm=newton', ' '.join(cmd))
 
     def test_args_algorithm_init_alpha(self):
-        args = OptimizeArgs(init_alpha=2e-4)
+        args = OptimizeArgs(algorithm='bfgs', init_alpha=2e-4)
         args.validate()
         cmd = args.compose(None, cmd=['output'])
 
         self.assertIn('init_alpha=0.0002', ' '.join(cmd))
+        args = OptimizeArgs(init_alpha=2e-4)
+        with self.assertRaises(ValueError):
+            args.validate()
         args = OptimizeArgs(init_alpha=-1.0)
         with self.assertRaises(ValueError):
             args.validate()
diff --git a/test/test_model.py b/test/test_model.py
index 7144a6d..4c3e2cc 100644
--- a/test/test_model.py
+++ b/test/test_model.py
@@ -299,13 +299,8 @@ class CmdStanModelTest(CustomTestCase):
 
     def test_model_syntax_error(self):
         stan = os.path.join(DATAFILES_PATH, 'bad_syntax.stan')
-        with LogCapture(level=logging.WARNING) as log:
-            logging.getLogger()
-            with self.assertRaises(ValueError):
-                CmdStanModel(stan_file=stan)
-        log.check_present(
-            ('cmdstanpy', 'WARNING', StringComparison(r'(?s).*Syntax error.*'))
-        )
+        with self.assertRaisesRegex(ValueError, r'.*Syntax error.*'):
+            CmdStanModel(stan_file=stan)
 
     def test_repr(self):
         model = CmdStanModel(stan_file=BERN_STAN)
diff --git a/test/test_optimize.py b/test/test_optimize.py
index ff62b50..3792234 100644
--- a/test/test_optimize.py
+++ b/test/test_optimize.py
@@ -432,9 +432,7 @@ class OptimizeTest(unittest.TestCase):
         jdata = os.path.join(DATAFILES_PATH, 'bernoulli.data.json')
         jinit = os.path.join(DATAFILES_PATH, 'bernoulli.init.json')
 
-        with self.assertRaisesRegex(
-            ValueError, 'must not be set when algorithm is Newton'
-        ):
+        with self.assertRaisesRegex(ValueError, 'bfgs or lbfgs'):
             model.optimize(
                 data=jdata,
                 seed=1239812093,
@@ -443,9 +441,7 @@ class OptimizeTest(unittest.TestCase):
                 tol_obj=1,
             )
 
-        with self.assertRaisesRegex(
-            ValueError, 'must not be set when algorithm is Newton'
-        ):
+        with self.assertRaisesRegex(ValueError, 'bfgs or lbfgs'):
             model.optimize(
                 data=jdata,
                 seed=1239812093,
@@ -454,9 +450,7 @@ class OptimizeTest(unittest.TestCase):
                 tol_rel_obj=1,
             )
 
-        with self.assertRaisesRegex(
-            ValueError, 'must not be set when algorithm is Newton'
-        ):
+        with self.assertRaisesRegex(ValueError, 'bfgs or lbfgs'):
             model.optimize(
                 data=jdata,
                 seed=1239812093,
@@ -465,9 +459,7 @@ class OptimizeTest(unittest.TestCase):
                 tol_grad=1,
             )
 
-        with self.assertRaisesRegex(
-            ValueError, 'must not be set when algorithm is Newton'
-        ):
+        with self.assertRaisesRegex(ValueError, 'bfgs or lbfgs'):
             model.optimize(
                 data=jdata,
                 seed=1239812093,
@@ -476,9 +468,15 @@ class OptimizeTest(unittest.TestCase):
                 tol_rel_grad=1,
             )
 
-        with self.assertRaisesRegex(
-            ValueError, 'must not be set when algorithm is Newton'
-        ):
+        with self.assertRaisesRegex(ValueError, 'bfgs or lbfgs'):
+            model.optimize(
+                data=jdata,
+                seed=1239812093,
+                inits=jinit,
+                tol_rel_grad=1,
+            )
+
+        with self.assertRaisesRegex(ValueError, 'bfgs or lbfgs'):
             model.optimize(
                 data=jdata,
                 seed=1239812093,
@@ -489,7 +487,7 @@ class OptimizeTest(unittest.TestCase):
 
         with self.assertRaisesRegex(
             ValueError,
-            'history_size must not be set when algorithm is Newton or BFGS',
+            'lbfgs',
         ):
             model.optimize(
                 data=jdata,
@@ -501,7 +499,7 @@ class OptimizeTest(unittest.TestCase):
 
         with self.assertRaisesRegex(
             ValueError,
-            'history_size must not be set when algorithm is Newton or BFGS',
+            'lbfgs',
         ):
             model.optimize(
                 data=jdata,
@@ -511,6 +509,17 @@ class OptimizeTest(unittest.TestCase):
                 history_size=1,
             )
 
+        with self.assertRaisesRegex(
+            ValueError,
+            'lbfgs',
+        ):
+            model.optimize(
+                data=jdata,
+                seed=1239812093,
+                inits=jinit,
+                history_size=1,
+            )
+
     def test_optimize_good_dict(self):
         exe = os.path.join(DATAFILES_PATH, 'bernoulli' + EXTENSION)
         stan = os.path.join(DATAFILES_PATH, 'bernoulli.stan')