def add_columns(self, layers: Union[np.ndarray, Dict[str, np.ndarray], loompy.LayerManager], col_attrs: Dict[str, np.ndarray], *, row_attrs: Dict[str, np.ndarray] = None, fill_values: Dict[str, np.ndarray] = None) -> None:
if self._file.mode != "<STR_LIT>":<EOL><INDENT>raise IOError("<STR_LIT>")<EOL><DEDENT>is_new = self.shape == (<NUM_LIT:0>, <NUM_LIT:0>)<EOL>if is_new:<EOL><INDENT>if row_attrs is None:<EOL><INDENT>raise ValueError("<STR_LIT>")<EOL><DEDENT>for k, v in row_attrs.items():<EOL><INDENT>self.ra[k] = v<EOL>self.shape = (self.ra[k].shape[<NUM_LIT:0>], self.shape[<NUM_LIT:1>])<EOL><DEDENT>if len(self.ca) == <NUM_LIT:0>:<EOL><INDENT>for k, v in col_attrs.items():<EOL><INDENT>self.ca[k] = np.zeros(<NUM_LIT:0>, v.dtype)<EOL><DEDENT><DEDENT><DEDENT>layers_dict: Dict[str, np.ndarray] = {}<EOL>if isinstance(layers, np.ndarray):<EOL><INDENT>layers_dict = {"<STR_LIT>": layers}<EOL><DEDENT>elif isinstance(layers, loompy.LayerManager):<EOL><INDENT>layers_dict = {k: v[:, :] for k, v in layers.items()}<EOL><DEDENT>elif isinstance(layers, dict):<EOL><INDENT>layers_dict = layers<EOL><DEDENT>else:<EOL><INDENT>raise ValueError("<STR_LIT>")<EOL><DEDENT>n_cols = <NUM_LIT:0><EOL>for layer, matrix in layers_dict.items():<EOL><INDENT>if not is_new and layer not in self.layers.keys():<EOL><INDENT>raise ValueError(f"<STR_LIT>")<EOL><DEDENT>if matrix.shape[<NUM_LIT:0>] != self.shape[<NUM_LIT:0>]:<EOL><INDENT>raise ValueError(f"<STR_LIT>")<EOL><DEDENT>if n_cols == <NUM_LIT:0>:<EOL><INDENT>n_cols = matrix.shape[<NUM_LIT:1>]<EOL><DEDENT>elif matrix.shape[<NUM_LIT:1>] != n_cols:<EOL><INDENT>raise ValueError(f"<STR_LIT>")<EOL><DEDENT><DEDENT>did_remove = False<EOL>todel = []  <EOL>for key, vals in col_attrs.items():<EOL><INDENT>if key not in self.col_attrs:<EOL><INDENT>if fill_values is not None:<EOL><INDENT>if fill_values == "<STR_LIT>":<EOL><INDENT>fill_with = np.zeros(<NUM_LIT:1>, dtype=col_attrs[key].dtype)[<NUM_LIT:0>]<EOL><DEDENT>else:<EOL><INDENT>fill_with = fill_values[key]<EOL><DEDENT>self.ca[key] = np.array([fill_with] * self.shape[<NUM_LIT:1>])<EOL><DEDENT>else:<EOL><INDENT>did_remove = True<EOL>todel.append(key)<EOL><DEDENT><DEDENT>if len(vals) != n_cols:<EOL><INDENT>raise ValueError(f"<STR_LIT>")<EOL><DEDENT><DEDENT>for key in todel:<EOL><INDENT>del col_attrs[key]<EOL><DEDENT>if did_remove:<EOL><INDENT>logging.debug("<STR_LIT>" + "<STR_LIT:U+002C>".join(todel))<EOL><DEDENT>todel = []<EOL>did_remove = False<EOL>for key in self.col_attrs.keys():<EOL><INDENT>if key not in col_attrs:<EOL><INDENT>if fill_values is not None:<EOL><INDENT>if fill_values == "<STR_LIT>":<EOL><INDENT>fill_with = np.zeros(<NUM_LIT:1>, dtype=self.col_attrs[key].dtype)[<NUM_LIT:0>]<EOL><DEDENT>else:<EOL><INDENT>fill_with = fill_values[key]<EOL><DEDENT>col_attrs[key] = np.array([fill_with] * n_cols)<EOL><DEDENT>else:<EOL><INDENT>did_remove = True<EOL>todel.append(key)<EOL><DEDENT><DEDENT><DEDENT>for key in todel:<EOL><INDENT>del self.ca[key]  <EOL><DEDENT>if did_remove:<EOL><INDENT>logging.debug("<STR_LIT>" + "<STR_LIT:U+002C>".join(todel))<EOL><DEDENT>if is_new:<EOL><INDENT>for k, v in layers_dict.items():<EOL><INDENT>self.layers[k] = v<EOL><DEDENT>for k, v in col_attrs.items():<EOL><INDENT>self.ca[k] = v<EOL><DEDENT><DEDENT>else:<EOL><INDENT>n_cols = n_cols + self.shape[<NUM_LIT:1>]<EOL>old_n_cols = self.shape[<NUM_LIT:1>]<EOL>self.shape = (self.shape[<NUM_LIT:0>], n_cols)<EOL>todel = []<EOL>for key, vals in col_attrs.items():<EOL><INDENT>if vals.shape[<NUM_LIT:1>:] != self.col_attrs[key].shape[<NUM_LIT:1>:]:<EOL><INDENT>logging.debug(f"<STR_LIT>")<EOL>todel.append(key)<EOL><DEDENT>else:<EOL><INDENT>self.ca[key] = np.concatenate([self.ca[key], vals])<EOL><DEDENT><DEDENT>for key in todel:<EOL><INDENT>del self.ca[key]<EOL><DEDENT>for key in self.layers.keys():<EOL><INDENT>self.layers[key]._resize(n_cols, axis=<NUM_LIT:1>)<EOL>self.layers[key][:, old_n_cols:n_cols] = layers_dict[key]<EOL><DEDENT><DEDENT>self._file.flush()<EOL>
Add columns of data and attribute values to the dataset.

Args:
        layers (dict or numpy.ndarray or LayerManager):
                Either:
                1) A N-by-M matrix of float32s (N rows, M columns) in this case columns are added at the default layer
                2) A dict {layer_name : matrix} specified so that the matrix (N, M) will be added to layer `layer_name`
                3) A LayerManager object (such as what is returned by view.layers)
        col_attrs (dict):
                Column attributes, where keys are attribute names and values are numpy arrays (float or string) of length M
        row_attrs (dict):
                Optional row attributes, where keys are attribute names and values are numpy arrays (float or string) of length M
        fill_values: dictionary of values to use if a column attribute is missing, or "auto" to fill with zeros or empty strings

Returns:
        Nothing.

Notes
-----
- This will modify the underlying HDF5 file, which will interfere with any concurrent readers.
- Column attributes in the file that are NOT provided, will be deleted (unless fill value provided).
- Array with Nan should not be provided