diff --git a/subtrees/dagflow/dagflow/bundles/load_parameters.py b/subtrees/dagflow/dagflow/bundles/load_parameters.py
index 2f8ddc34387e7ccd97247a0de0de5a419bd6e21e..1efa0405ef8cdb1ae50675e1d6e336f38eb0bd33 100644
--- a/subtrees/dagflow/dagflow/bundles/load_parameters.py
+++ b/subtrees/dagflow/dagflow/bundles/load_parameters.py
@@ -145,7 +145,7 @@ def iterate_varcfgs(cfg: NestedMKDict):
             varcfg['label'] = {}
         yield key, varcfg
 
-from dagflow.variable import Parameters
+from dagflow.parameters import Parameters
 from dagflow.lib.SumSq import SumSq
 
 def load_parameters(acfg):
diff --git a/subtrees/dagflow/dagflow/parameters.py b/subtrees/dagflow/dagflow/parameters.py
new file mode 100644
index 0000000000000000000000000000000000000000..0c218a0b2e1b2495d8926ecffa2e428ed16168c8
--- /dev/null
+++ b/subtrees/dagflow/dagflow/parameters.py
@@ -0,0 +1,462 @@
+from .node import Node, Output
+from .exception import InitializationError
+from .lib.NormalizeCorrelatedVars2 import NormalizeCorrelatedVars2
+from .lib.Cholesky import Cholesky
+from .lib.Array import Array
+from .lib.CovmatrixFromCormatrix import CovmatrixFromCormatrix
+
+from numpy import zeros_like, array
+from numpy.typing import DTypeLike
+from typing import Optional, Dict, List, Generator
+
+class Parameter:
+    __slots__ = ('_idx','_parent', '_value_output', '_labelfmt')
+    _parent: Optional['Parameters']
+    _idx: int
+    _value_output: Output
+    _labelfmt: str
+
+    def __init__(
+        self,
+        value_output: Output,
+        idx: int=0,
+        *,
+        parent: 'Parameters',
+        labelfmt: str='{}'
+    ):
+        self._idx = idx
+        self._parent = parent
+        self._value_output = value_output
+        self._labelfmt = labelfmt
+
+    @property
+    def value(self) -> float:
+        return self._value_output.data[self._idx]
+
+    @value.setter
+    def value(self, value: float):
+        return self._value_output.seti(self._idx, value)
+
+    @property
+    def output(self) -> Output:
+        return self._value_output
+
+    def label(self, source: str='text') -> str:
+        return self._labelfmt.format(self._value_output.node.label(source))
+
+    def to_dict(self, *, label_from: str='text') -> dict:
+        return {
+                'value': self.value,
+                'label': self.label(label_from)
+                }
+
+class GaussianParameter(Parameter):
+    __slots__ = ( '_central_output', '_sigma_output', '_normvalue_output')
+    _central_output: Output
+    _sigma_output: Output
+    _normvalue_output: Output
+
+    def __init__(
+        self,
+        value_output: Output,
+        central_output: Output,
+        sigma_output: Output,
+        idx: int=0,
+        *,
+        normvalue_output: Output,
+        **kwargs
+    ):
+        super().__init__(value_output, idx, **kwargs)
+        self._central_output = central_output
+        self._sigma_output = sigma_output
+        self._normvalue_output = normvalue_output
+
+    @property
+    def central(self) -> float:
+        return self._central_output.data[0]
+
+    @central.setter
+    def central(self, central: float):
+        self._central_output.seti(self._idx, central)
+
+    @property
+    def sigma(self) -> float:
+        return self._sigma_output.data[0]
+
+    @sigma.setter
+    def sigma(self, sigma: float):
+        self._sigma_output.seti(self._idx, sigma)
+
+    @property
+    def sigma_relative(self) -> float:
+        return self.sigma/self.value
+
+    @sigma_relative.setter
+    def sigma_relative(self, sigma_relative: float):
+        self.sigma = sigma_relative * self.value
+
+    @property
+    def sigma_percent(self) -> float:
+        return 100.0 * (self.sigma/self.value)
+
+    @sigma_percent.setter
+    def sigma_percent(self, sigma_percent: float):
+        self.sigma = (0.01*sigma_percent) * self.value
+
+    @property
+    def normvalue(self) -> float:
+        return self._normvalue_output.data[0]
+
+    @normvalue.setter
+    def normvalue(self, normvalue: float):
+        self._normvalue_output.seti(self._idx, normvalue)
+
+    def to_dict(self, **kwargs) -> dict:
+        dct = super().to_dict(**kwargs)
+        dct.update({
+            'central': self.central,
+            'sigma': self.sigma,
+            # 'normvalue': self.normvalue,
+            })
+        return dct
+
+class NormalizedGaussianParameter(Parameter):
+    @property
+    def central(self) -> float:
+        return 0.0
+
+    @property
+    def sigma(self) -> float:
+        return 1.0
+
+    def to_dict(self, **kwargs) -> dict:
+        dct = super().to_dict(**kwargs)
+        dct.update({
+            'central': 0.0,
+            'sigma': 1.0,
+            # 'normvalue': self.value,
+            })
+        return dct
+
+class Constraint:
+    __slots__ = ('_pars')
+    _pars: "Parameters"
+
+    def __init__(self, parameters: "Parameters"):
+        self._pars = parameters
+
+class Parameters:
+    __slots__ = (
+        'value',
+        '_value_node',
+        '_pars',
+        '_norm_pars',
+        '_is_variable',
+        '_constraint'
+    )
+    value: Output
+    _value_node: Node
+    _pars: List[Parameter]
+    _norm_pars: List[Parameter]
+
+    _is_variable: bool
+
+    _constraint: Optional[Constraint]
+
+    def __init__(
+        self,
+        value: Node,
+        *,
+        variable: Optional[bool]=None,
+        fixed: Optional[bool]=None,
+        close: bool=True
+    ):
+        self._value_node = value
+        self.value = value.outputs[0]
+
+        if all(f is not None for f in (variable, fixed)):
+            raise RuntimeError("Parameter may not be set to variable and fixed at the same time")
+        if variable is not None:
+            self._is_variable = variable
+        elif fixed is not None:
+            self._is_variable = not fixed
+        else:
+            self._is_variable = True
+
+        self._constraint = None
+
+        self._pars = []
+        self._norm_pars = []
+        if close:
+            self._close()
+
+            for i in range(self.value._data.size):
+                self._pars.append(Parameter(self.value, i, parent=self))
+
+    def _close(self) -> None:
+        self._value_node.close(recursive=True)
+
+    @property
+    def is_variable(self) -> bool:
+        return self._is_variable
+
+    @property
+    def is_fixed(self) -> bool:
+        return not self._is_variable
+
+    @property
+    def is_constrained(self) -> bool:
+        return self._constraint is not None
+
+    @property
+    def is_free(self) -> bool:
+        return self._constraint is None
+
+    @property
+    def parameters(self) -> List:
+        return self._pars
+
+    @property
+    def norm_parameters(self) -> List:
+        return self._norm_pars
+
+    @property
+    def constraint(self) -> Optional[Constraint]:
+        return self._constraint
+
+    def to_dict(self, *, label_from: str='text') -> dict:
+        return {
+                'value': self.value.data[0],
+                'label': self._value_node.label(label_from)
+                }
+
+    def set_constraint(self, constraint: Constraint) -> None:
+        if self._constraint is not None:
+            raise InitializationError("Constraint already set")
+        self._constraint = constraint
+        # constraint._pars = self
+
+    @staticmethod
+    def from_numbers(
+        value: float,
+        *,
+        dtype: DTypeLike='d',
+        variable: Optional[bool]=None,
+        fixed: Optional[bool]=None,
+        label: Optional[Dict[str, str]]=None,
+        **kwargs
+    ) -> 'Parameters':
+        if label is None:
+            label = {'text': 'parameter'}
+        else:
+            label = dict(label)
+        name: str = label.setdefault('name', 'parameter')
+        has_constraint = kwargs.get('sigma', None) is not None
+        pars = Parameters(
+            Array(
+                name,
+                array((value,), dtype=dtype),
+                label = label,
+                mode='store_weak',
+            ),
+            fixed=fixed,
+            variable=variable,
+            close=not has_constraint
+        )
+
+        if has_constraint:
+            pars.set_constraint(
+                GaussianConstraint.from_numbers(
+                    parameters=pars,
+                    dtype=dtype,
+                    **kwargs
+                )
+            )
+            pars._close()
+
+        return pars
+
+class GaussianConstraint(Constraint):
+    __slots__ = (
+        'central', 'sigma', 'normvalue',
+        '_central_node', '_sigma_node', '_normvalue_node',
+        '_cholesky_node', '_covariance_node', '_correlation_node',
+        '_sigma_total_node',
+        '_norm_node',
+        '_is_constrained'
+    )
+    central: Output
+    sigma: Output
+    normvalue: Output
+
+    _central_node: Node
+    _sigma_node: Node
+    _normvalue_node: Node
+
+    _cholesky_node: Optional[Node]
+    _covariance_node: Optional[Node]
+    _correlation_node: Optional[Node]
+    _sigma_total_node: Optional[Node]
+
+    _norm_node: Node
+
+    _is_constrained: bool
+
+    def __init__(
+        self,
+        central: Node,
+        *,
+        parameters: Parameters,
+        sigma: Node=None,
+        covariance: Node=None,
+        correlation: Node=None,
+        constrained: Optional[bool]=None,
+        free: Optional[bool]=None,
+        **_
+    ):
+        super().__init__(parameters=parameters)
+        self._central_node = central
+
+        self._cholesky_node = None
+        self._covariance_node = None
+        self._correlation_node = None
+        self._sigma_total_node = None
+
+        if all(f is not None for f in (constrained, free)):
+            raise RuntimeError("GaussianConstraint may not be set to constrained and free at the same time")
+        if constrained is not None:
+            self._is_constrained = constrained
+        elif free is not None:
+            self._is_constrained = not free
+        else:
+            self._is_constrained = True
+
+        if sigma is not None and covariance is not None:
+            raise InitializationError('GaussianConstraint: got both "sigma" and "covariance" as arguments')
+        if correlation is not None and sigma is None:
+            raise InitializationError('GaussianConstraint: got "correlation", but no "sigma" as arguments')
+
+        value_node = parameters._value_node
+        if correlation is not None:
+            self._correlation_node = correlation
+            self._covariance_node = CovmatrixFromCormatrix(f"V({value_node.name})")
+            self._cholesky_node = Cholesky(f"L({value_node.name})")
+            self._sigma_total_node = sigma
+            self._sigma_node = self._cholesky_node
+
+            self._sigma_total_node >> self._covariance_node.inputs['sigma']
+            correlation >> self._covariance_node
+            self._covariance_node >> self._cholesky_node
+        elif sigma is not None:
+            self._sigma_node = sigma
+        elif covariance is not None:
+            self._cholesky_node = Cholesky(f"L({value_node.name})")
+            self._sigma_node = self._cholesky_node
+            self._covariance_node = covariance
+
+            covariance >> self._cholesky_node
+        else:
+            # TODO: no sigma/covariance AND central means normalized=value?
+            raise InitializationError('GaussianConstraint: got no "sigma" and no "covariance" arguments')
+
+        self.central = self._central_node.outputs[0]
+        self.sigma = self._sigma_node.outputs[0]
+
+        self._normvalue_node = Array(
+            f'Normalized {value_node.name}',
+            zeros_like(self.central._data),
+            mark = f'norm({value_node.mark})',
+            mode='store_weak'
+        )
+        self._normvalue_node._inherit_labels(self._pars._value_node, fmt='Normalized {}')
+        self.normvalue = self._normvalue_node.outputs[0]
+
+        self._norm_node = NormalizeCorrelatedVars2(f"Normalize {value_node.name}", immediate=True)
+        self.central >> self._norm_node.inputs['central']
+        self.sigma >> self._norm_node.inputs['matrix']
+
+        (parameters.value, self.normvalue) >> self._norm_node
+
+        self._norm_node.close(recursive=True)
+        self._norm_node.touch()
+
+        value_output = self._pars.value
+        for i in range(value_output._data.size):
+            self._pars._pars.append(
+                GaussianParameter(
+                    value_output,
+                    self.central,
+                    self.sigma,
+                    i,
+                    normvalue_output=self.normvalue,
+                    parent=self
+                )
+            )
+            self._pars._norm_pars.append(
+                NormalizedGaussianParameter(
+                    self.normvalue,
+                    i,
+                    parent=self,
+                    labelfmt='[norm] {}'
+                )
+            )
+
+    @property
+    def is_constrained(self) -> bool:
+        return self._is_constrained
+
+    @property
+    def is_free(self) -> bool:
+        return not self._is_constrained
+
+    @property
+    def is_correlated(self) -> bool:
+        return not self._covariance_node is not None
+
+    @staticmethod
+    def from_numbers(
+        *,
+        central: float,
+        sigma: float,
+        label: Optional[Dict[str,str]]=None,
+        dtype: DTypeLike='d',
+        **kwargs
+    ) -> 'GaussianParameters':
+        if label is None:
+            label = {'text': 'gaussian parameter'}
+        else:
+            label = dict(label)
+        name = label.setdefault('name', 'parameter')
+
+        node_central = Array(
+            f'{name}_central',
+            array((central,), dtype=dtype),
+            label = {k: f'central: {v}' for k,v in label.items()},
+            mode='store_weak'
+        )
+
+        node_sigma = Array(
+            f'{name}_sigma',
+            array((sigma,), dtype=dtype),
+            label = {k: f'sigma: {v}' for k,v in label.items()},
+            mode='store_weak'
+        )
+
+        return GaussianConstraint(central=node_central, sigma=node_sigma, **kwargs)
+
+    def to_dict(self, **kwargs) -> dict:
+        dct = super().to_dict(**kwargs)
+        dct.update({
+            'central': self.central.data[0],
+            'sigma': self.sigma.data[0],
+            # 'normvalue': self.normvalue.data[0],
+            })
+        return dct
+
+def GaussianParameters(value: Node, *args, **kwargs) -> Parameters:
+    pars = Parameters(value, close=False)
+    pars.set_constraint(GaussianConstraint(*args, parameters=pars, **kwargs))
+    pars._close()
+
+    return pars
+
diff --git a/subtrees/dagflow/test/variables/test_load_variables.py b/subtrees/dagflow/test/parameters/test_load_parameters.py
similarity index 100%
rename from subtrees/dagflow/test/variables/test_load_variables.py
rename to subtrees/dagflow/test/parameters/test_load_parameters.py
diff --git a/subtrees/dagflow/test/variables/test_variables.py b/subtrees/dagflow/test/parameters/test_parameters.py
similarity index 92%
rename from subtrees/dagflow/test/variables/test_variables.py
rename to subtrees/dagflow/test/parameters/test_parameters.py
index 149ac3c566d33f1d2c96dc5bd6c86181f822035f..0e17d6bc93b8764b11a480c655cd2cb1a950f6ff 100644
--- a/subtrees/dagflow/test/variables/test_variables.py
+++ b/subtrees/dagflow/test/parameters/test_parameters.py
@@ -1,7 +1,7 @@
 #!/usr/bin/env python
 
 from dagflow.lib import Array
-from dagflow.variable import GaussianParameters
+from dagflow.parameters import GaussianParameters
 from dagflow.graph import Graph
 from dagflow.graphviz import savegraph
 from dagflow.exception import CriticalError
@@ -61,19 +61,19 @@ def test_variables_00_variable(mode) -> None:
         raise error
 
     value_out0 = gp.value.data.copy()
-    normvalue_out0 = gp.normvalue.data
+    normvalue_out0 = gp.constraint.normvalue.data
     assert allclose(value_in, value_out0, atol=0, rtol=0)
     assert all(normvalue_out0!=0)
 
-    gp.normvalue.set(zeros_in)
+    gp.constraint.normvalue.set(zeros_in)
     value_out1 = gp.value.data
-    normvalue_out1 = gp.normvalue.data
+    normvalue_out1 = gp.constraint.normvalue.data
     assert allclose(central_in, value_out1, atol=0, rtol=0)
     assert allclose(normvalue_out1, 0.0, atol=0, rtol=0)
 
     gp.value.set(value_out0)
     value_out2 = gp.value.data
-    normvalue_out2 = gp.normvalue.data
+    normvalue_out2 = gp.constraint.normvalue.data
     assert allclose(value_in, value_out2, atol=0, rtol=0)
     assert allclose(normvalue_out2, normvalue_out0, atol=0, rtol=0)