Repository: ICB-DCM/pyPESTO

Issue:
Profiles incompatible with fixed parameters (Old: Profiles are per default computed for all including fixed indices)
I think the proper default behavior would be to only compute profiles for free parameters. Opinions?

Discussion:
dweindl: 
![Codacy](https://app.codacy.com/assets/images/favicon.png) Here is an overview of what got changed by this pull request:


```diff

Complexity increasing per file
==============================
- test/test_problem.py  3
         

Complexity decreasing per file
==============================
+ pypesto/problem.py  -2
         

Clones added
============
- pypesto/problem.py  2
         
```


See the complete overview on [Codacy](https://app.codacy.com/manual/dweindl/pyPESTO/pullRequest?prid=5683834&bid=18604937)
        
dweindl: 
![Codacy](https://app.codacy.com/assets/images/favicon.png) Here is an overview of what got changed by this pull request:


```diff

Complexity increasing per file
==============================
- test/test_problem.py  3
         

Complexity decreasing per file
==============================
+ pypesto/problem.py  -2
         

Clones added
============
- pypesto/problem.py  2
         
```


See the complete overview on [Codacy](https://app.codacy.com/manual/dweindl/pyPESTO/pullRequest?prid=5683834&bid=18604937)
        
dweindl: 
![Codacy](https://app.codacy.com/assets/images/favicon.png) Here is an overview of what got changed by this pull request:


```diff

Complexity increasing per file
==============================
- test/test_problem.py  3
         

Complexity decreasing per file
==============================
+ pypesto/problem.py  -2
         

Clones added
============
- pypesto/problem.py  2
         
```


See the complete overview on [Codacy](https://app.codacy.com/manual/dweindl/pyPESTO/pullRequest?prid=5683834&bid=18604937)
        
dweindl: 
![Codacy](https://app.codacy.com/assets/images/favicon.png) Here is an overview of what got changed by this pull request:


```diff

Complexity increasing per file
==============================
- test/test_problem.py  3
         

Complexity decreasing per file
==============================
+ pypesto/problem.py  -2
         

Clones added
============
- pypesto/problem.py  2
         
```


See the complete overview on [Codacy](https://app.codacy.com/manual/dweindl/pyPESTO/pullRequest?prid=5683834&bid=18604937)
        
dweindl: 
![Codacy](https://app.codacy.com/assets/images/favicon.png) Here is an overview of what got changed by this pull request:


```diff

Complexity increasing per file
==============================
- test/test_problem.py  3
         

Complexity decreasing per file
==============================
+ pypesto/problem.py  -2
         

Clones added
============
- pypesto/problem.py  2
         
```


See the complete overview on [Codacy](https://app.codacy.com/manual/dweindl/pyPESTO/pullRequest?prid=5683834&bid=18604937)
        
dweindl: 
![Codacy](https://app.codacy.com/assets/images/favicon.png) Here is an overview of what got changed by this pull request:


```diff

Complexity increasing per file
==============================
- test/test_problem.py  3
         

Complexity decreasing per file
==============================
+ pypesto/problem.py  -2
         

Clones added
============
- pypesto/problem.py  2
         
```


See the complete overview on [Codacy](https://app.codacy.com/manual/dweindl/pyPESTO/pullRequest?prid=5683834&bid=18604937)
        
dweindl: 
![Codacy](https://app.codacy.com/assets/images/favicon.png) Here is an overview of what got changed by this pull request:


```diff

Complexity increasing per file
==============================
- test/test_problem.py  3
         

Complexity decreasing per file
==============================
+ pypesto/problem.py  -2
         

Clones added
============
- pypesto/problem.py  2
         
```


See the complete overview on [Codacy](https://app.codacy.com/manual/dweindl/pyPESTO/pullRequest?prid=5683834&bid=18604937)
        
dweindl: 
![Codacy](https://app.codacy.com/assets/images/favicon.png) Here is an overview of what got changed by this pull request:


```diff

Complexity increasing per file
==============================
- test/test_problem.py  3
         

Complexity decreasing per file
==============================
+ pypesto/problem.py  -2
         

Clones added
============
- pypesto/problem.py  2
         
```


See the complete overview on [Codacy](https://app.codacy.com/manual/dweindl/pyPESTO/pullRequest?prid=5683834&bid=18604937)
        
dweindl: 
![Codacy](https://app.codacy.com/assets/images/favicon.png) Here is an overview of what got changed by this pull request:


```diff

Complexity increasing per file
==============================
- test/test_problem.py  3
- pypesto/storage/save_to_hdf5.py  1
         

Complexity decreasing per file
==============================
+ pypesto/problem.py  -2
         

Clones added
============
- pypesto/problem.py  2
         
```


See the complete overview on [Codacy](https://app.codacy.com/manual/dweindl/pyPESTO/pullRequest?prid=5683834&bid=18604937)
        
dweindl: 
![Codacy](https://app.codacy.com/assets/images/favicon.png) Here is an overview of what got changed by this pull request:


```diff

Complexity increasing per file
==============================
- pypesto/storage/save_to_hdf5.py  1
- test/test_problem.py  3
         

Complexity decreasing per file
==============================
+ pypesto/problem.py  -2
         

Clones added
============
- pypesto/problem.py  2
         
```


See the complete overview on [Codacy](https://app.codacy.com/manual/dweindl/pyPESTO/pullRequest?prid=5683834&bid=18604937)
        
codecov-commenter: # [Codecov](https://codecov.io/gh/ICB-DCM/pyPESTO/pull/393?src=pr&el=h1) Report
> Merging [#393](https://codecov.io/gh/ICB-DCM/pyPESTO/pull/393?src=pr&el=desc) into [develop](https://codecov.io/gh/ICB-DCM/pyPESTO/commit/f4ce876b68d518d102c11a37c27df1027b18db6e&el=desc) will **decrease** coverage by `0.15%`.
> The diff coverage is `90.90%`.

[![Impacted file tree graph](https://codecov.io/gh/ICB-DCM/pyPESTO/pull/393/graphs/tree.svg?width=650&height=150&src=pr&token=vaUsU0G1LY)](https://codecov.io/gh/ICB-DCM/pyPESTO/pull/393?src=pr&el=tree)

```diff
@@             Coverage Diff             @@
##           develop     #393      +/-   ##
===========================================
- Coverage    91.23%   91.07%   -0.16%     
===========================================
  Files           67       67              
  Lines         3788     3789       +1     
===========================================
- Hits          3456     3451       -5     
- Misses         332      338       +6     
```


| [Impacted Files](https://codecov.io/gh/ICB-DCM/pyPESTO/pull/393?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [pypesto/storage/save\_to\_hdf5.py](https://codecov.io/gh/ICB-DCM/pyPESTO/pull/393/diff?src=pr&el=tree#diff-cHlwZXN0by9zdG9yYWdlL3NhdmVfdG9faGRmNS5weQ==) | `93.10% <ø> (ø)` | |
| [pypesto/visualize/profiles.py](https://codecov.io/gh/ICB-DCM/pyPESTO/pull/393/diff?src=pr&el=tree#diff-cHlwZXN0by92aXN1YWxpemUvcHJvZmlsZXMucHk=) | `93.57% <ø> (ø)` | |
| [pypesto/problem.py](https://codecov.io/gh/ICB-DCM/pyPESTO/pull/393/diff?src=pr&el=tree#diff-cHlwZXN0by9wcm9ibGVtLnB5) | `91.48% <88.57%> (-0.49%)` | :arrow_down: |
| [pypesto/profile/profile\_next\_guess.py](https://codecov.io/gh/ICB-DCM/pyPESTO/pull/393/diff?src=pr&el=tree#diff-cHlwZXN0by9wcm9maWxlL3Byb2ZpbGVfbmV4dF9ndWVzcy5weQ==) | `93.38% <100.00%> (-0.16%)` | :arrow_down: |
| [pypesto/objective/amici\_util.py](https://codecov.io/gh/ICB-DCM/pyPESTO/pull/393/diff?src=pr&el=tree#diff-cHlwZXN0by9vYmplY3RpdmUvYW1pY2lfdXRpbC5weQ==) | `82.27% <0.00%> (-5.07%)` | :arrow_down: |
| [pypesto/objective/amici\_calculator.py](https://codecov.io/gh/ICB-DCM/pyPESTO/pull/393/diff?src=pr&el=tree#diff-cHlwZXN0by9vYmplY3RpdmUvYW1pY2lfY2FsY3VsYXRvci5weQ==) | `95.23% <0.00%> (-1.59%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/ICB-DCM/pyPESTO/pull/393?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/ICB-DCM/pyPESTO/pull/393?src=pr&el=footer). Last update [f4ce876...d1f9f7c](https://codecov.io/gh/ICB-DCM/pyPESTO/pull/393?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

dweindl: 
![Codacy](https://app.codacy.com/assets/images/favicon.png) Here is an overview of what got changed by this pull request:


```diff

Complexity increasing per file
==============================
- test/test_problem.py  3
- pypesto/storage/save_to_hdf5.py  1
         

Complexity decreasing per file
==============================
+ pypesto/problem.py  -3
         

Clones added
============
- pypesto/problem.py  2
         
```


See the complete overview on [Codacy](https://app.codacy.com/manual/dweindl/pyPESTO/pullRequest?prid=5683834&bid=18604937)
        
dweindl: 
![Codacy](https://app.codacy.com/assets/images/favicon.png) Here is an overview of what got changed by this pull request:


```diff

Complexity increasing per file
==============================
- test/test_problem.py  3
- pypesto/storage/save_to_hdf5.py  1
         

Complexity decreasing per file
==============================
+ pypesto/problem.py  -3
         

Clones added
============
- pypesto/problem.py  2
         
```


See the complete overview on [Codacy](https://app.codacy.com/manual/dweindl/pyPESTO/pullRequest?prid=5683834&bid=18604937)
        
paulstapor: Seems like visualization doesn't work yet with fixed parameters... :/
yannikschaelte: > Seems like visualization doesn't work yet with fixed parameters... :/

True. This should be adressed here too then.
dweindl: 
![Codacy](https://app.codacy.com/assets/images/favicon.png) Here is an overview of what got changed by this pull request:


```diff

Complexity increasing per file
==============================
- pypesto/storage/save_to_hdf5.py  1
- test/test_problem.py  3
         

Complexity decreasing per file
==============================
+ pypesto/storage/read_from_hdf5.py  -1
+ pypesto/problem.py  -3
         
```


See the complete overview on [Codacy](https://app.codacy.com/manual/dweindl/pyPESTO/pullRequest?prid=5683834&bid=18604937)
        
dweindl: 
![Codacy](https://app.codacy.com/assets/images/favicon.png) Here is an overview of what got changed by this pull request:


```diff

Complexity increasing per file
==============================
- pypesto/storage/save_to_hdf5.py  1
- test/test_problem.py  3
         

Complexity decreasing per file
==============================
+ pypesto/problem.py  -3
+ pypesto/storage/read_from_hdf5.py  -1
         
```


See the complete overview on [Codacy](https://app.codacy.com/manual/dweindl/pyPESTO/pullRequest?prid=5683834&bid=18604937)
        
dweindl: 
![Codacy](https://app.codacy.com/assets/images/favicon.png) Here is an overview of what got changed by this pull request:


```diff

Complexity increasing per file
==============================
- test/test_problem.py  3
- pypesto/storage/save_to_hdf5.py  1
         

Complexity decreasing per file
==============================
+ pypesto/problem.py  -3
+ pypesto/storage/read_from_hdf5.py  -1
         
```


See the complete overview on [Codacy](https://app.codacy.com/manual/dweindl/pyPESTO/pullRequest?prid=5683834&bid=18604937)
        
paulstapor: Okay, it seems like the whole story becomes a bit more interesting:
If a parameter is fixed, there will be no profile computed, hence the profilerResult object in the result list is `None`.

It would be easy just to pick those profiles in the profile list, which have been computed and simply plot those. Yet...:
Assume somebody computes a profile for parameter A, and then fixes this parameter. Should the profile still be plotted?
In other words: Do we want to plot all parameter profiles, which have been computed, or restrict furthermore to those, which are not fixed?
yannikschaelte: > It would be easy just to pick those profiles in the profile list, which have been computed and simply plot those. Yet...:

I would say taking the entries in the result.profile_result[i] that are not None is a good default.

> Assume somebody computes a profile for parameter A, and then fixes this parameter. Should the profile still be plotted?

If the profile exists, it can be plotted as above. And fixing post-hoc should be rather uncommon, I would say ...

> In other words: Do we want to plot all parameter profiles, which have been computed, or restrict furthermore to those, which are not fixed?

I would say the former.


dweindl: 
![Codacy](https://app.codacy.com/assets/images/favicon.png) Here is an overview of what got changed by this pull request:


```diff

Complexity increasing per file
==============================
- pypesto/storage/save_to_hdf5.py  1
- test/test_problem.py  3
         

Complexity decreasing per file
==============================
+ pypesto/problem.py  -3
+ pypesto/storage/read_from_hdf5.py  -1
         
```


See the complete overview on [Codacy](https://app.codacy.com/manual/dweindl/pyPESTO/pullRequest?prid=5683834&bid=18604937)
        
dweindl: 
![Codacy](https://app.codacy.com/assets/images/favicon.png) Here is an overview of what got changed by this pull request:


```diff

Complexity increasing per file
==============================
- test/test_problem.py  3
- pypesto/storage/save_to_hdf5.py  1
         

Complexity decreasing per file
==============================
+ pypesto/storage/read_from_hdf5.py  -1
+ pypesto/problem.py  -3
         
```


See the complete overview on [Codacy](https://app.codacy.com/manual/dweindl/pyPESTO/pullRequest?prid=5683834&bid=18604937)
        

Patch:
diff --git a/pypesto/problem.py b/pypesto/problem.py
index bfaafea..5b07302 100644
--- a/pypesto/problem.py
+++ b/pypesto/problem.py
@@ -55,10 +55,6 @@ class Problem:
     x_priors_defs:
         Definitions of priors for parameters. Types of priors, and their
         required and optional parameters, are described in the `Prior` class.
-
-    Attributes
-    ----------
-
     dim:
         The number of non-fixed parameters.
         Computed from the other values.
@@ -92,16 +88,12 @@ class Problem:
                  x_names: Optional[Iterable[str]] = None,
                  x_scales: Optional[Iterable[str]] = None,
                  x_priors_defs: Optional[NegLogPriors] = None):
-
         self.objective = copy.deepcopy(objective)
 
-        self.lb = np.array(lb).flatten()
-        self.ub = np.array(ub).flatten()
-
         self.lb_full = np.array(lb).flatten()
         self.ub_full = np.array(ub).flatten()
 
-        self.dim_full = dim_full if dim_full is not None else self.lb.size
+        self.dim_full = dim_full if dim_full is not None else self.lb_full.size
 
         if x_fixed_indices is None:
             x_fixed_indices = []
@@ -115,14 +107,11 @@ class Problem:
             x_fixed_vals = [x_fixed_vals]
         self.x_fixed_vals: List[float] = [float(x) for x in x_fixed_vals]
 
-        self.dim: int = self.dim_full - len(self.x_fixed_indices)
-
-        self.x_free_indices: List[int] = sorted(
-            set(range(0, self.dim_full)) - set(self.x_fixed_indices))
+        self._x_free_indices: Union[List[int], None] = None
 
         if x_guesses is None:
-            x_guesses = np.zeros((0, self.dim))
-        self.x_guesses: np.ndarray = np.array(x_guesses)
+            x_guesses = np.zeros((0, self.dim_full))
+        self.x_guesses_full: np.ndarray = np.array(x_guesses)
 
         if objective.x_names is not None:
             x_names = objective.x_names
@@ -136,28 +125,53 @@ class Problem:
 
         self.x_priors = x_priors_defs
 
-        self.normalize_input()
+        self.normalize()
+
+    @property
+    def lb(self) -> np.ndarray:
+        return self.lb_full[self.x_free_indices]
+
+    @property
+    def ub(self) -> np.ndarray:
+        return self.ub_full[self.x_free_indices]
+
+    @property
+    def x_guesses(self) -> np.ndarray:
+        return self.x_guesses_full[:, self.x_free_indices]
+
+    @property
+    def dim(self) -> int:
+        return self.dim_full - len(self.x_fixed_indices)
 
-    def normalize_input(self, check_x_guesses: bool = True) -> None:
+    @property
+    def x_free_indices(self) -> List[int]:
+        return sorted(set(range(0, self.dim_full)) - set(self.x_fixed_indices))
+
+    def normalize(self) -> None:
         """
         Reduce all vectors to dimension dim and have the objective accept
         vectors of dimension dim.
         """
 
-        if self.lb.size == self.dim_full:
-            self.lb = self.lb[self.x_free_indices]
-        elif self.lb.size == 1:
-            self.lb = self.lb * np.ones(self.dim)
-            self.lb_full = self.lb * np.ones(self.dim_full)
-
-        if self.ub.size == self.dim_full:
-            self.ub = self.ub[self.x_free_indices]
-        elif self.ub.size == 1:
-            self.ub = self.ub * np.ones(self.dim)
-            self.ub_full = self.ub * np.ones(self.dim_full)
-
-        if self.x_guesses.shape[1] == self.dim_full:
-            self.x_guesses = self.x_guesses[:, self.x_free_indices]
+        if self.lb_full.size == 1:
+            self.lb_full = self.lb_full * np.ones(self.dim_full)
+        elif self.lb_full.size != self.dim_full:
+            self.lb_full = np.empty(self.dim_full)
+            self.lb_full[self.x_free_indices] = self.lb
+            self.lb_full[self.x_fixed_indices] = self.x_fixed_vals
+
+        if self.ub_full.size == 1:
+            self.ub_full = self.ub_full * np.ones(self.dim_full)
+        elif self.ub_full.size != self.dim_full:
+            self.ub_full = np.empty(self.dim_full)
+            self.ub_full[self.x_free_indices] = self.ub
+            self.ub_full[self.x_fixed_indices] = self.x_fixed_vals
+
+        if self.x_guesses_full.shape[1] != self.dim_full:
+            x_guesses = np.empty((self.x_guesses_full.shape[0], self.dim_full))
+            x_guesses[:] = np.nan
+            x_guesses[:, self.x_free_indices] = self.x_guesses_full
+            self.x_guesses_full = x_guesses
 
         # make objective aware of fixed parameters
         self.objective.update_from_problem(
@@ -167,19 +181,12 @@ class Problem:
             x_fixed_vals=self.x_fixed_vals)
 
         # sanity checks
-        if self.lb.size != self.dim:
-            raise AssertionError("lb dimension invalid.")
-        if self.ub.size != self.dim:
-            raise AssertionError("ub dimension invalid.")
         if self.lb_full.size != self.dim_full:
             raise AssertionError("lb_full dimension invalid.")
         if self.ub_full.size != self.dim_full:
             raise AssertionError("ub_full dimension invalid.")
         if len(self.x_scales) != self.dim_full:
             raise AssertionError("x_scales dimension invalid.")
-        if check_x_guesses:
-            if self.x_guesses.shape[1] != self.dim:
-                raise AssertionError("x_guesses form invalid.")
         if len(self.x_names) != self.dim_full:
             raise AssertionError("x_names must be of length dim_full.")
         if len(self.x_fixed_indices) != len(self.x_fixed_vals):
@@ -200,7 +207,7 @@ class Problem:
         if not isinstance(parameter_vals, list):
             parameter_vals = [parameter_vals]
 
-        # first clean to be fixed indices to avoid redundancies
+        # first clean to-be-fixed indices to avoid redundancies
         for i_index, i_parameter in enumerate(parameter_indices):
             # check if parameter was already fixed, otherwise add it to the
             # fixed parameters
@@ -212,14 +219,7 @@ class Problem:
                 self.x_fixed_indices.append(i_parameter)
                 self.x_fixed_vals.append(parameter_vals[i_index])
 
-        self.dim = self.dim_full - len(self.x_fixed_indices)
-
-        self.x_free_indices = [
-            int(i) for i in
-            set(range(0, self.dim_full)) - set(self.x_fixed_indices)
-        ]
-
-        self.normalize_input()
+        self.normalize()
 
     def unfix_parameters(
             self,
@@ -232,24 +232,14 @@ class Problem:
         if not isinstance(parameter_indices, list):
             parameter_indices = [parameter_indices]
 
-        # first clean to be freed indices
-        for i_index, i_parameter in enumerate(parameter_indices):
+        # first clean to-be-freed indices
+        for i_parameter in parameter_indices:
             if i_parameter in self.x_fixed_indices:
+                i_index = self.x_fixed_indices.index(i_parameter)
                 self.x_fixed_indices.pop(i_index)
                 self.x_fixed_vals.pop(i_index)
 
-        self.dim = self.dim_full - len(self.x_fixed_indices)
-
-        self.x_free_indices = [
-            int(i) for i in
-            set(range(0, self.dim_full)) - set(self.x_fixed_indices)
-        ]
-
-        # readapt bounds
-        self.lb = self.lb_full[self.x_free_indices]
-        self.ub = self.ub_full[self.x_free_indices]
-
-        self.normalize_input(check_x_guesses=False)
+        self.normalize()
 
     def get_full_vector(
             self,
diff --git a/pypesto/profile/profile_next_guess.py b/pypesto/profile/profile_next_guess.py
index 00d0548..b3e152c 100644
--- a/pypesto/profile/profile_next_guess.py
+++ b/pypesto/profile/profile_next_guess.py
@@ -77,7 +77,7 @@ def fixed_step(
     Parameters
     ----------
     x:
-       The current position of the profiler
+       The current position of the profiler, size `dim_full`.
     par_index:
         The index of the parameter of the current profile
     par_direction:
@@ -86,6 +86,11 @@ def fixed_step(
         Various options applied to the profile optimization.
     problem:
         The problem to be solved.
+
+    Returns
+    -------
+    x_new:
+        The updated parameter vector, of size `dim_full`.
     """
     delta_x = np.zeros(len(x))
     delta_x[par_index] = par_direction * options.default_step_size
@@ -117,26 +122,31 @@ def adaptive_step(
 
     Parameters
     ----------
-     x:
-         The current position of the profiler
-     par_index:
-         The index of the parameter of the current profile
-     par_direction:
-         The direction, in which the profiling is done (1 or -1)
-     options:
-         Various options applied to the profile optimization.
-     current_profile:
-         The profile which should be computed
-     problem:
-         The problem to be solved.
-     global_opt:
-         log-posterior value of the global optimum
-     order:
-         Specifies the precise algorithm for extrapolation: can be 0 (
-         just one parameter is updated), 1 (last two points used to
-         extrapolate all parameters), and np.nan (indicates that a more
-         complex regression should be used)
-     """
+    x:
+        The current position of the profiler, size `dim_full`.
+    par_index:
+        The index of the parameter of the current profile
+    par_direction:
+        The direction, in which the profiling is done (1 or -1)
+    options:
+        Various options applied to the profile optimization.
+    current_profile:
+        The profile which should be computed
+    problem:
+        The problem to be solved.
+    global_opt:
+        log-posterior value of the global optimum
+    order:
+        Specifies the precise algorithm for extrapolation: can be 0 (
+        just one parameter is updated), 1 (last two points used to
+        extrapolate all parameters), and np.nan (indicates that a more
+        complex regression should be used)
+
+    Returns
+    -------
+    x_new:
+        The updated parameter vector, of size `dim_full`.
+    """
     # restrict step proposal to minimum and maximum step size
     def clip_to_minmax(step_size_proposal):
         return clip(step_size_proposal, options.min_step_size,
@@ -148,13 +158,12 @@ def adaptive_step(
 
     # check if this is the first step
     n_profile_points = len(current_profile.fval_path)
-    pos_ind_red = len([ip for ip in problem.x_free_indices if ip < par_index])
     problem.fix_parameters(par_index, x[par_index])
 
     # Get update directions and first step size guesses
     (step_size_guess, delta_x_dir, reg_par, delta_obj_value) = \
         handle_profile_history(x, par_index, par_direction, n_profile_points,
-                               pos_ind_red, global_opt, order,
+                               global_opt, order,
                                current_profile, problem, options)
 
     # check whether we must make a minimum step anyway, since we're close to
@@ -175,12 +184,15 @@ def adaptive_step(
         if np.isnan(order) and n_profile_points > 2:
             x_step_tmp = []
             # loop over parameters, extrapolate each one
-            for i_par in range(len(problem.x_free_indices) + 1):
-                if i_par == pos_ind_red:
+            for i_par in range(problem.dim_full):
+                if i_par == par_index:
                     # if we meet the profiling parameter, just increase,
                     # don't extrapolate
                     x_step_tmp.append(x[par_index] + step_length *
                                       par_direction)
+                elif i_par in problem.x_fixed_indices:
+                    # common fixed parameter: will be ignored anyway later
+                    x_step_tmp.append(np.nan)
                 else:
                     # extrapolate
                     cur_par_extrapol = np.poly1d(reg_par[i_par])
@@ -205,7 +217,7 @@ def adaptive_step(
 
     # compute objective at the guessed point
     problem.fix_parameters(par_index, next_x[par_index])
-    next_obj = problem.objective(reduce_x(next_x, par_index))
+    next_obj = problem.objective(problem.get_reduced_vector(next_x))
 
     # iterate until good step size is found
     if next_obj_target < next_obj:
@@ -228,7 +240,6 @@ def handle_profile_history(
         par_index: int,
         par_direction: int,
         n_profile_points: int,
-        pos_ind_red: int,
         global_opt: float,
         order: int,
         current_profile: ProfilerResult,
@@ -268,7 +279,7 @@ def handle_profile_history(
         elif np.isnan(order):
             # compute the regression polynomial for parameter extrapolation
 
-            reg_par = get_reg_polynomial(n_profile_points, pos_ind_red,
+            reg_par = get_reg_polynomial(n_profile_points,
                                          par_index, current_profile,
                                          problem, options)
 
@@ -277,7 +288,6 @@ def handle_profile_history(
 
 def get_reg_polynomial(
         n_profile_points: int,
-        pos_ind_red: int,
         par_index: int,
         current_profile: ProfilerResult,
         problem: Problem,
@@ -295,10 +305,10 @@ def get_reg_polynomial(
 
     # set up matrix of regression parameters
     reg_par = []
-    for i_par in range(len(problem.x_free_indices) + 1):
-        if i_par == pos_ind_red:
-            # if we meet the current profiling parameter, there is nothing
-            # to do, so pass an np.nan
+    for i_par in range(problem.dim_full):
+        if i_par in problem.x_fixed_indices:
+            # if we meet the current profiling parameter or a fixed parameter,
+            # there is nothing to do, so pass an np.nan
             reg_par.append(np.nan)
         else:
             # Do polynomial interpolation of profile path
@@ -366,7 +376,7 @@ def do_line_seach(
             # compute new objective value
             problem.fix_parameters(par_index, next_x[par_index])
             last_obj = copy.copy(next_obj)
-            next_obj = problem.objective(reduce_x(next_x, par_index))
+            next_obj = problem.objective(problem.get_reduced_vector(next_x))
 
             # check for root crossing and compute correct step size in case
             if direction == 'decrease' and next_obj_target >= next_obj:
@@ -395,15 +405,6 @@ def next_x_interpolate(
     return last_x + add_x
 
 
-def reduce_x(next_x: np.ndarray, par_index: int) -> np.ndarray:
-    """
-    Reduce step proposal to non-fixed parameters.
-    """
-    red_ind = list(range(len(next_x)))
-    red_ind.pop(par_index)
-    return np.array([next_x[ip] for ip in red_ind])
-
-
 def clip(
         vector_guess: Union[float, np.ndarray],
         lower: Union[float, np.ndarray],
diff --git a/pypesto/storage/read_from_hdf5.py b/pypesto/storage/read_from_hdf5.py
index 8c8a542..6c50521 100644
--- a/pypesto/storage/read_from_hdf5.py
+++ b/pypesto/storage/read_from_hdf5.py
@@ -80,7 +80,6 @@ class ProblemHDF5Reader:
         # h5 uses numpy for everything; convert to lists where necessary
         problem.x_fixed_vals = [float(val) for val in problem.x_fixed_vals]
         problem.x_fixed_indices = [int(ix) for ix in problem.x_fixed_indices]
-        problem.x_free_indices = [int(ix) for ix in problem.x_free_indices]
         problem.x_names = [str(name) for name in problem.x_names]
 
         return problem
diff --git a/pypesto/storage/save_to_hdf5.py b/pypesto/storage/save_to_hdf5.py
index 340441d..3ee9ff5 100644
--- a/pypesto/storage/save_to_hdf5.py
+++ b/pypesto/storage/save_to_hdf5.py
@@ -50,7 +50,8 @@ class ProblemHDF5Writer:
                                     "If you wish to overwrite the file set"
                                     "overwrite=True.")
             attrs_to_save = [a for a in dir(problem) if not a.startswith('__')
-                             and not callable(getattr(problem, a))]
+                             and not callable(getattr(problem, a))
+                             and not hasattr(type(problem), a)]
 
             problem_grp = f.create_group("problem")
             # problem_grp.attrs['config'] = objective.get_config()
diff --git a/pypesto/visualize/misc.py b/pypesto/visualize/misc.py
index 4214527..488167a 100644
--- a/pypesto/visualize/misc.py
+++ b/pypesto/visualize/misc.py
@@ -8,7 +8,7 @@ from typing import Optional
 
 def process_result_list(results, colors=None, legends=None):
     """
-    assigns colors and legends to a list of results, chekc user provided lists
+    assigns colors and legends to a list of results, check user provided lists
 
     Parameters
     ----------
diff --git a/pypesto/visualize/profiles.py b/pypesto/visualize/profiles.py
index 77f8eb5..34b56dd 100644
--- a/pypesto/visualize/profiles.py
+++ b/pypesto/visualize/profiles.py
@@ -2,6 +2,7 @@ import matplotlib.pyplot as plt
 from matplotlib.ticker import MaxNLocator
 import numpy as np
 from typing import Sequence, Tuple
+from warnings import warn
 
 from ..result import Result
 from .reference_points import create_references
@@ -53,14 +54,28 @@ def profiles(results, ax=None, profile_indices=None, size=(18.5, 6.5),
     # parse input
     (results, colors, legends) = process_result_list(results, colors, legends)
 
-    # get the correct number of parameter indices, even if not the same in
-    # all result objects
+    # get all parameter indices, for which profiles were computed
+    plottable_indices = set()
+    for result in results:
+        # get parameter indices, for which profiles were computed
+        tmp_indices = [
+            par_id for par_id, prof in
+            enumerate(result.profile_result.list[profile_list_id])
+            if prof is not None]
+        # profile_indices should contain all parameter indices,
+        # for which in at least one of the results a profile exists
+        plottable_indices.update(tmp_indices)
+    plottable_indices = sorted(plottable_indices)
+
+    # get the profiles, which should be plotted and sanitize, if not plottable
     if profile_indices is None:
-        profile_indices = []
-        for result in results:
-            tmp_indices = list(range(len(
-                result.profile_result.list[profile_list_id])))
-            profile_indices = list(set().union(profile_indices, tmp_indices))
+        profile_indices = plottable_indices
+    else:
+        for ind in profile_indices:
+            if ind not in plottable_indices:
+                profile_indices.remove(ind)
+                warn('Requested to plot profile for parameter index %i, '
+                     'but profile has not been computed.' % ind)
 
     # loop over results
     for j, result in enumerate(results):
@@ -326,7 +341,8 @@ def handle_inputs(
     # extract ratio values values from result
     fvals = []
     for i_par in range(0, len(result.profile_result.list[profile_list])):
-        if i_par in profile_indices:
+        if i_par in profile_indices and \
+                result.profile_result.list[profile_list][i_par] is not None:
             xs = result.profile_result.list[profile_list][i_par]\
                 .x_path[i_par, :]
             ratios = result.profile_result.list[profile_list][i_par]\


Test patch:
diff --git a/test/__init__.py b/test/__init__.py
index e69de29..0475078 100644
--- a/test/__init__.py
+++ b/test/__init__.py
@@ -0,0 +1,1 @@
+"""Unit tests for pyPESTO."""
diff --git a/test/test_problem.py b/test/test_problem.py
index 516a4a3..0ea2d7b 100644
--- a/test/test_problem.py
+++ b/test/test_problem.py
@@ -5,6 +5,7 @@ import pypesto
 
 @pytest.fixture
 def problem():
+    """A very basic problem."""
     lb = [-5] * 10
     ub = [6] * 10
     objective = pypesto.Objective()
@@ -14,7 +15,38 @@ def problem():
     return problem
 
 
+def test_dim_vs_dim_full():
+    """Test passing arrays in the full or reduced dimension."""
+    objective = pypesto.Objective()
+
+    # define problem with bounds including fixed parameters
+    pypesto.Problem(
+        objective=objective, lb=[-1] * 4, ub=[1] * 4, dim_full=4,
+        x_fixed_indices=[0, 3], x_fixed_vals=[42, 43]
+    )
+
+    # define problem with incomplete bounds
+    pypesto.Problem(
+        objective=objective, lb=[-1] * 2, ub=[1] * 2, dim_full=4,
+        x_fixed_indices=[0, 3], x_fixed_vals=[42, 43]
+    )
+
+
+def test_fix_parameters(problem):
+    """Test the dynamic problem parameter fixing functions."""
+    problem.fix_parameters(2, 45)
+    assert problem.x_fixed_indices == [0, 1, 5, 2]
+    assert problem.x_fixed_vals == [42, 43, 44, 45]
+    assert problem.x_free_indices == [3, 4, 6, 7, 8, 9]
+
+    problem.unfix_parameters(2)
+    assert problem.x_fixed_indices == [0, 1, 5]
+    assert problem.x_fixed_vals == [42, 43, 44]
+    assert problem.x_free_indices == [2, 3, 4, 6, 7, 8, 9]
+
+
 def test_full_index_to_free_index(problem):
+    """Test problem.full_index_to_free_index."""
     assert problem.full_index_to_free_index(2) == 0
     assert problem.full_index_to_free_index(6) == 3
     with pytest.raises(ValueError):
diff --git a/test/test_profile.py b/test/test_profile.py
index 4f07ff5..b5f08bb 100644
--- a/test/test_profile.py
+++ b/test/test_profile.py
@@ -10,6 +10,7 @@ from copy import deepcopy
 import warnings
 
 from pypesto import ObjectiveBase
+from .visualize import close_fig
 
 
 class ProfilerTest(unittest.TestCase):
@@ -25,6 +26,7 @@ class ProfilerTest(unittest.TestCase):
             (cls.problem, cls.result, cls.optimizer) = \
                 create_optimization_results(cls.objective)
 
+    @close_fig
     def test_default_profiling(self):
         # loop over  methods for creating new initial guesses
         method_list = ['fixed_step', 'adaptive_step_order_0',
@@ -61,6 +63,10 @@ class ProfilerTest(unittest.TestCase):
                 self.assertTrue(steps > 1, 'Profiling with 0th order based '
                                            'proposal needed not enough steps.')
 
+            # standard plotting
+            pypesto.visualize.profiles(result, profile_list_id=i_run)
+            pypesto.visualize.profile_cis(result, profile_list=i_run)
+
     def test_selected_profiling(self):
         # create options in order to ensure a short computation time
         options = pypesto.ProfileOptions(default_step_size=0.02,
@@ -118,8 +124,8 @@ class ProfilerTest(unittest.TestCase):
                                            next_guess_method='fixed_step')
 
         # set new bounds (knowing that one parameter stopped at the bounds
-        self.problem.lb = -4 * np.ones(2)
-        self.problem.ub = 4 * np.ones(2)
+        self.problem.lb_full = -4 * np.ones(2)
+        self.problem.ub_full = 4 * np.ones(2)
 
         # re-run profiling using new bounds
         result = pypesto.parameter_profile(problem=self.problem,
@@ -153,7 +159,7 @@ class ProfilerTest(unittest.TestCase):
         # with pre-defined hessian
         result = deepcopy(self.result)
         result.optimize_result.list[0].hess = np.array([[2, 0], [0, 1]])
-        result = pypesto.profile.approximate_parameter_profile(
+        pypesto.profile.approximate_parameter_profile(
             problem=self.problem, result=result, profile_index=[0, 1],
             n_steps=n_steps)
 
@@ -184,6 +190,34 @@ def test_profile_with_history():
     )
 
 
+@close_fig
+def test_profile_with_fixed_parameters():
+    """Test using profiles with fixed parameters."""
+    obj = test_objective.rosen_for_sensi(max_sensi_order=1)['obj']
+
+    lb = -2 * np.ones(5)
+    ub = 2 * np.ones(5)
+    problem = pypesto.Problem(
+        objective=obj, lb=lb, ub=ub,
+        x_fixed_vals=[0.5, -1.8], x_fixed_indices=[0, 3])
+
+    optimizer = pypesto.ScipyOptimizer(options={'maxiter': 50})
+    result = pypesto.minimize(problem=problem, optimizer=optimizer, n_starts=2)
+
+    for i_method, next_guess_method in enumerate([
+            'fixed_step', 'adaptive_step_order_0',
+            'adaptive_step_order_1', 'adaptive_step_regression']):
+        print(next_guess_method)
+        pypesto.parameter_profile(
+            problem=problem, result=result, optimizer=optimizer,
+            next_guess_method=next_guess_method)
+
+        # standard plotting
+        axes = pypesto.visualize.profiles(result, profile_list_id=i_method)
+        assert len(axes) == 3
+        pypesto.visualize.profile_cis(result, profile_list=i_method)
+
+
 def create_optimization_results(objective):
     # create optimizer, pypesto problem and options
     options = {
diff --git a/test/visualize/__init__.py b/test/visualize/__init__.py
new file mode 100644
index 0000000..8ddd6cc
--- /dev/null
+++ b/test/visualize/__init__.py
@@ -0,0 +1,3 @@
+"""Visualization tests."""
+
+from .test_visualize import close_fig