Repository: Zuzu-Typ/PyGLM (language: python)

Issue:
few enhancements suggestions in the API
Hello that's me again !

With time, I have been collecting many small notices I had on the types and functions present in this great module. Here I would like to report back my thoughts about possible nice improvements :slightly_smiling_face: 
I know some of the designs my suggestions might be more related to [glm](https://github.com/g-truc/glm) than to `pyglm` ... but still !

## 1. mat <-> quat conversion

The way to convert matrices to quaternion is very smooth:  `quat(mat3(...))`
However the conversion from quat to mat is weird in my mind:  `mat3_cast(quat(...))` or `mat4_cast(quat(...))`
I would think that if a `quat` can be created directly from a matrix, with no extra function, it should be the same back to `mat`
Also conversion `quat->mat` is non-ambiguous at contrary to `mat->quat` which can be subject to interpretation when `mat` is non orthogonal. So I would say if some of both conversions should be a separated function, it should be `mat->quat` and not `quat->mat`

## 2. affine transformation for vectors

Half the operations I do with `mat4` is transforming a `vec3`, but as they are not of compatible dimensions there is always the need to convert them. Could it be possible to have a function doing this multiplication ?
```python
>>> vec3(matrix * vec4(v,1))    # many conversions and parenthesis for just one vector
>>> hmul(matrix, v)   # all conversions made in C code, so much more efficient
```
(I thought to `hmul` for Homogeneous Multiplication because `affineMul` like `affineInverse` seemed too cumbersome)

As i suspect it could be useful in other dimensions so it could be an overloaded functions:
```python
hmul(mat4, vec3) -> vec3
hmul(mat3, vec2) -> vec2
hmul(mat2, vec1) -> vec1
```

## 3. have `glm.min/max` work on iterables, like the python builtin functions

I think it wouldn't change a lot efficiency, but do you think it could be possible to have `glm.min/max` work with iterables of vectors ?
Currently I am frequently in situation where I need to do this
```python
vmin = vec3(inf)
for p in points:
    vmin = glm.min(vmin, p)

# instead of
vmin = glm.min(points)
```

making it working on iterables would make `glm.min/max` perfect vectorized replacements for the builtins `min/max`

## 4. rotation and translation functions

I fail to understand the exact meaning of the current `glm.rotate` and `glm.translate` functions :thinking: 
I have been using it in the early times of [my module](https://github.com/jimy-byerley/pymadcad) but found them error prone and hard to read mixed in other codes.

seeing [this example](https://github.com/g-truc/glm/blob/master/manual.md#-11-using-global-headers), It seems to be meant to apply successive transformations to a matrix, but why should it be done by composing those functions (which is hard to read), rather than multiplying the transformation matrices. Affine matrices are meant for it after all ?
I would have imagined something like
```python
htranslation(offset:vec3) -> mat4     # returning a mat4 doing only a translation
hrotation(angle:float, axis:vec3) -> mat4  # returning a mat4 doing only a rotation
hrotation(quat) -> mat4
hrotationx(angle:float) -> mat4  # rotation around X
hrotationy(angle:float) -> mat4  # rotation around Y
hrotationz(angle:float) -> mat4  # rotation around Z

# same as in the above glm example (If I correctly understood it):
>>> view = hrotation(orientation.x, X) * hrotation(orientation.y, Y) * htranslation(translate)
```


# conclusion

I hope I haven't been too out of topic with my suggestions. Else what do you think about these ?

cheers, Byerley


Discussion / hints:
> ## 1. mat <-> quat conversion
> The way to convert matrices to quaternion is very smooth: `quat(mat3(...))`
> However the conversion from quat to mat is weird in my mind: `mat3_cast(quat(...))` or `mat4_cast(quat(...))`
> I would think that if a `quat` can be created directly from a matrix, with no extra function, it should be the same back to `mat`

Yeah. I agree. This would be more logical.



> ## 2. affine transformation for vectors
> Half the operations I do with `mat4` is transforming a `vec3`, but as they are not of compatible dimensions there is always the need to convert them. Could it be possible to have a function doing this multiplication ?

This seems like a more complex feature to implement.
I'll see what I can do.
Could you please make a separate Issue for this?



> ## 3. have `glm.min/max` work on iterables, like the python builtin functions
> I think it wouldn't change a lot efficiency, but do you think it could be possible to have `glm.min/max` work with iterables of vectors ?
> Currently I am frequently in situation where I need to do this
> 
> ```python
> vmin = vec3(inf)
> for p in points:
>     vmin = glm.min(vmin, p)
> 
> # instead of
> vmin = glm.min(points)
> ```

I think this would be a nice feature.
For the time being, here's a slight improvement:
```Python
import functools

functools.reduce(glm.min, points)
```



> ## 4. rotation and translation functions
> I fail to understand the exact meaning of the current `glm.rotate` and `glm.translate` functions 🤔

It's actually simpler than you might think.
`glm.rotate(m, angle, axis)` creates a rotation matrix around the given axis (e.g. `vec3(0, 1, 0)`), by the given `angle` and the input matrix `m` is multiplied by this newly created rotation matrix.
It's basically a shorthand for:
```Python

# The hrotation function you suggested:
hrotation = lambda angle, axis: glm.rotate(glm.mat4(), angle, axis)

def rotate(m, angle, axis):
    return m * hrotation(angle, axis)
```

I don't know though why there is no overloaded version of `rotate()` or `translate()` that omits the input matrix.

---

Thank you for the suggestions!
I'll have to see when I have enough free time to implement them (:

Cheers
--Zuzu_Typ--
I opened a separated issue for suggestion 2. as you asked :)

Fix patch:
diff --git a/PyGLM/functions/detail/func_common.h b/PyGLM/functions/detail/func_common.h
index cff96f2..d74f9d2 100644
--- a/PyGLM/functions/detail/func_common.h
+++ b/PyGLM/functions/detail/func_common.h
@@ -110,12 +110,158 @@ PyDoc_STRVAR(min_docstr,
 	"min(a: number, b: number, c: number, d: number) -> float\n"
 	"	Returns the minimum value of 4 inputs.\n"
 	"min(a: vecN, b: vecN, c: vecN, d: vecN) -> vecN\n"
-	"	Returns the minimum component wise value of 4 inputs."
+	"	Returns the minimum component wise value of 4 inputs.\n"
+	"min(iterable) -> any\n"
+	"	Returns the smallest number or the minimum component wise value respectively."
 );
+
+template<typename T>
+static T
+apply_min(std::vector<T>& items) {
+	T minimum = items[0];
+	for (T item : items) {
+		if (item == minimum) {
+			continue;
+		}
+		minimum = glm::min(item, minimum);
+	}
+	return minimum;
+}
+
+template<typename T>
+static inline PyObject*
+apply_min_from_PyObject_number_vector(std::vector<PyObject*>& items) {
+	std::vector<T> itemsAsT = std::vector<T>(items.size());
+
+	for (size_t i = 0; i < items.size(); i++) {
+		PyObject* item = items[i];
+		itemsAsT[i] = PyGLM_Number_FromPyObject<T>(item);
+		Py_DECREF(item);
+	}
+
+	return pack(apply_min(itemsAsT));
+}
+
+template<int L, typename T>
+static inline PyObject*
+apply_min_from_PyObject_vector_vector(std::vector<PyObject*>& items) {
+	std::vector<glm::vec<L, T>> itemsAsVecT = std::vector<glm::vec<L, T>>(items.size());
+
+	for (size_t i = 0; i < items.size(); i++) {
+		PyObject* item = items[i];
+		itemsAsVecT[i] = reinterpret_cast<vec<L, T>*>(item)->super_type;
+		Py_DECREF(item);
+	}
+
+	return pack(apply_min(itemsAsVecT));
+}
+
+#define min_GEN_TYPE_SWITCH_STATEMENT_FOR_VECTOR(L) \
+switch (pti->format) { \
+case get_format_specifier<float>(): \
+	return apply_min_from_PyObject_vector_vector<L, float>(items); \
+case get_format_specifier<double>(): \
+	return apply_min_from_PyObject_vector_vector<L, double>(items); \
+case get_format_specifier<int32>(): \
+	return apply_min_from_PyObject_vector_vector<L, int32>(items); \
+case get_format_specifier<uint32>(): \
+	return apply_min_from_PyObject_vector_vector<L, uint32>(items); \
+case get_format_specifier<int64>(): \
+	return apply_min_from_PyObject_vector_vector<L, int64>(items); \
+case get_format_specifier<uint64>(): \
+	return apply_min_from_PyObject_vector_vector<L, uint64>(items); \
+case get_format_specifier<int16>(): \
+	return apply_min_from_PyObject_vector_vector<L, int16>(items); \
+case get_format_specifier<uint16>(): \
+	return apply_min_from_PyObject_vector_vector<L, uint16>(items); \
+case get_format_specifier<int8>(): \
+	return apply_min_from_PyObject_vector_vector<L, int8>(items); \
+case get_format_specifier<uint8>(): \
+	return apply_min_from_PyObject_vector_vector<L, uint8>(items); \
+case get_format_specifier<bool>(): \
+	return apply_min_from_PyObject_vector_vector<L, bool>(items); \
+}
+
 static PyObject*
 min_(PyObject*, PyObject* args) {
-	PyObject *arg1, *arg2, *arg3 = NULL, *arg4 = NULL;
-	if (!PyArg_UnpackTuple(args, "min", 2, 4, &arg1, &arg2, &arg3, &arg4)) return NULL;
+	PyObject *arg1, *arg2 = NULL, *arg3 = NULL, *arg4 = NULL;
+	if (!PyArg_UnpackTuple(args, "min", 1, 4, &arg1, &arg2, &arg3, &arg4)) return NULL;
+	if (arg2 == NULL) {
+		if (PyObject_IterCheck(arg1)) {
+			PyObject* iterator = PyObject_GetIter(arg1);
+
+			std::vector<PyObject*> items{};
+
+			PyTypeObject* type = NULL;
+
+			PyObject* currentItem;
+			while ((currentItem = PyIter_Next(iterator))) {
+				if (type == NULL) {
+					if ((currentItem->ob_type->tp_dealloc != (destructor)vec_dealloc) && !PyLong_Check(currentItem) && !PyFloat_Check(currentItem) && !PyBool_Check(currentItem)) {
+						Py_DECREF(currentItem);
+						Py_DECREF(iterator);
+						PyGLM_TYPEERROR_O("invalid item type(s) for 'min()'. Expected number or PyGLM type, got ", currentItem);
+						return NULL;
+					}
+					type = Py_TYPE(currentItem);
+				}
+				else if (type != Py_TYPE(currentItem)) {
+					Py_DECREF(currentItem);
+					for (PyObject* item : items) {
+						Py_DECREF(item);
+					}
+					Py_DECREF(iterator);
+					PyGLM_TYPEERROR_2O("invalid item type(s) for 'min()'. Expected items of equal type, got ", currentItem, items[0]);
+					return NULL;
+				}
+				items.push_back(currentItem);
+			}
+
+			Py_DECREF(iterator);
+
+			if (PyErr_Occurred()) {
+				for (PyObject* item : items) {
+					Py_DECREF(item);
+				}
+				return NULL;
+			}
+
+			if (items.size() == 0) {
+				PyErr_SetString(PyExc_ValueError, "invalid argument value for 'min()'. Iterable has no items.");
+				return NULL;
+			}
+
+			if (type == &PyLong_Type) {
+				return apply_min_from_PyObject_number_vector<long>(items);
+			}
+			else if (type == &PyFloat_Type) {
+				return apply_min_from_PyObject_number_vector<double>(items);
+			}
+			else if (type == &PyBool_Type) {
+				return apply_min_from_PyObject_number_vector<bool>(items);
+			}
+			else {
+				PyGLMTypeObject* pti = (PyGLMTypeObject*)type;
+
+				switch (pti->C) {
+				case 1:
+					min_GEN_TYPE_SWITCH_STATEMENT_FOR_VECTOR(1);
+					break;
+				case 2:
+					min_GEN_TYPE_SWITCH_STATEMENT_FOR_VECTOR(2);
+					break;
+				case 3:
+					min_GEN_TYPE_SWITCH_STATEMENT_FOR_VECTOR(3);
+					break;
+				case 4:
+					min_GEN_TYPE_SWITCH_STATEMENT_FOR_VECTOR(4);
+					break;
+				}
+			}
+		}
+		PyErr_SetString(PyExc_TypeError, "invalid argument type(s) for min()");
+		return NULL;
+	}
 	if (arg3 == NULL) {
 		if (PyGLM_Number_Check(arg1) && PyGLM_Number_Check(arg2)) {
 			return pack(glm::min(PyGLM_Number_FromPyObject<double>(arg1), PyGLM_Number_FromPyObject<double>(arg2)));
@@ -652,12 +798,157 @@ PyDoc_STRVAR(max_docstr,
 	"max(a: number, b: number, c: number, d: number) -> float\n"
 	"	Returns the maximum value of 4 inputs.\n"
 	"max(a: vecN, b: vecN, c: vecN, d: vecN) -> vecN\n"
-	"	Returns the maximum component wise value of 4 inputs."
+	"	Returns the maximum component wise value of 4 inputs.\n"
+	"max(iterable) -> any\n"
+	"	Returns the greatest number or the maximum component wise value respectively."
 );
+template<typename T>
+static T
+apply_max(std::vector<T>& items) {
+	T maximum = items[0];
+	for (T item : items) {
+		if (item == maximum) {
+			continue;
+		}
+		maximum = glm::max(item, maximum);
+	}
+	return maximum;
+}
+
+template<typename T>
+static inline PyObject*
+apply_max_from_PyObject_number_vector(std::vector<PyObject*>& items) {
+	std::vector<T> itemsAsT = std::vector<T>(items.size());
+
+	for (size_t i = 0; i < items.size(); i++) {
+		PyObject* item = items[i];
+		itemsAsT[i] = PyGLM_Number_FromPyObject<T>(item);
+		Py_DECREF(item);
+	}
+
+	return pack(apply_max(itemsAsT));
+}
+
+template<int L, typename T>
+static inline PyObject*
+apply_max_from_PyObject_vector_vector(std::vector<PyObject*>& items) {
+	std::vector<glm::vec<L, T>> itemsAsVecT = std::vector<glm::vec<L, T>>(items.size());
+
+	for (size_t i = 0; i < items.size(); i++) {
+		PyObject* item = items[i];
+		itemsAsVecT[i] = reinterpret_cast<vec<L, T>*>(item)->super_type;
+		Py_DECREF(item);
+	}
+
+	return pack(apply_max(itemsAsVecT));
+}
+
+#define max_GEN_TYPE_SWITCH_STATEMENT_FOR_VECTOR(L) \
+switch (pti->format) { \
+case get_format_specifier<float>(): \
+	return apply_max_from_PyObject_vector_vector<L, float>(items); \
+case get_format_specifier<double>(): \
+	return apply_max_from_PyObject_vector_vector<L, double>(items); \
+case get_format_specifier<int32>(): \
+	return apply_max_from_PyObject_vector_vector<L, int32>(items); \
+case get_format_specifier<uint32>(): \
+	return apply_max_from_PyObject_vector_vector<L, uint32>(items); \
+case get_format_specifier<int64>(): \
+	return apply_max_from_PyObject_vector_vector<L, int64>(items); \
+case get_format_specifier<uint64>(): \
+	return apply_max_from_PyObject_vector_vector<L, uint64>(items); \
+case get_format_specifier<int16>(): \
+	return apply_max_from_PyObject_vector_vector<L, int16>(items); \
+case get_format_specifier<uint16>(): \
+	return apply_max_from_PyObject_vector_vector<L, uint16>(items); \
+case get_format_specifier<int8>(): \
+	return apply_max_from_PyObject_vector_vector<L, int8>(items); \
+case get_format_specifier<uint8>(): \
+	return apply_max_from_PyObject_vector_vector<L, uint8>(items); \
+case get_format_specifier<bool>(): \
+	return apply_max_from_PyObject_vector_vector<L, bool>(items); \
+}
+
 static PyObject*
 max_(PyObject*, PyObject* args) {
-	PyObject *arg1, *arg2, *arg3 = NULL, *arg4 = NULL;
-	if (!PyArg_UnpackTuple(args, "max", 2, 4, &arg1, &arg2, &arg3, &arg4)) return NULL;
+	PyObject *arg1, *arg2 = NULL, *arg3 = NULL, *arg4 = NULL;
+	if (!PyArg_UnpackTuple(args, "max", 1, 4, &arg1, &arg2, &arg3, &arg4)) return NULL;
+	if (arg2 == NULL) {
+		if (PyObject_IterCheck(arg1)) {
+			PyObject* iterator = PyObject_GetIter(arg1);
+
+			std::vector<PyObject*> items{};
+
+			PyTypeObject* type = NULL;
+
+			PyObject* currentItem;
+			while ((currentItem = PyIter_Next(iterator))) {
+				if (type == NULL) {
+					if ((currentItem->ob_type->tp_dealloc != (destructor)vec_dealloc) && !PyLong_Check(currentItem) && !PyFloat_Check(currentItem) && !PyBool_Check(currentItem)) {
+						Py_DECREF(currentItem);
+						Py_DECREF(iterator);
+						PyGLM_TYPEERROR_O("invalid item type(s) for 'max()'. Expected number or PyGLM type, got ", currentItem);
+						return NULL;
+					}
+					type = Py_TYPE(currentItem);
+				}
+				else if (type != Py_TYPE(currentItem)) {
+					Py_DECREF(currentItem);
+					for (PyObject* item : items) {
+						Py_DECREF(item);
+					}
+					Py_DECREF(iterator);
+					PyGLM_TYPEERROR_2O("invalid item type(s) for 'max()'. Expected items of equal type, got ", currentItem, items[0]);
+					return NULL;
+				}
+				items.push_back(currentItem);
+			}
+
+			Py_DECREF(iterator);
+
+			if (PyErr_Occurred()) {
+				for (PyObject* item : items) {
+					Py_DECREF(item);
+				}
+				return NULL;
+			}
+
+			if (items.size() == 0) {
+				PyErr_SetString(PyExc_ValueError, "invalid argument value for 'max()'. Iterable has no items.");
+				return NULL;
+			}
+
+			if (type == &PyLong_Type) {
+				return apply_max_from_PyObject_number_vector<long>(items);
+			}
+			else if (type == &PyFloat_Type) {
+				return apply_max_from_PyObject_number_vector<double>(items);
+			}
+			else if (type == &PyBool_Type) {
+				return apply_max_from_PyObject_number_vector<bool>(items);
+			}
+			else {
+				PyGLMTypeObject* pti = (PyGLMTypeObject*)type;
+
+				switch (pti->C) {
+				case 1:
+					max_GEN_TYPE_SWITCH_STATEMENT_FOR_VECTOR(1);
+					break;
+				case 2:
+					max_GEN_TYPE_SWITCH_STATEMENT_FOR_VECTOR(2);
+					break;
+				case 3:
+					max_GEN_TYPE_SWITCH_STATEMENT_FOR_VECTOR(3);
+					break;
+				case 4:
+					max_GEN_TYPE_SWITCH_STATEMENT_FOR_VECTOR(4);
+					break;
+				}
+			}
+		}
+		PyErr_SetString(PyExc_TypeError, "invalid argument type(s) for max()");
+		return NULL;
+	}
 	if (arg3 == NULL) {
 		if (PyGLM_Number_Check(arg1) && PyGLM_Number_Check(arg2)) {
 			return pack(glm::max(PyGLM_Number_FromPyObject<double>(arg1), PyGLM_Number_FromPyObject<double>(arg2)));
diff --git a/PyGLM/internal_functions/packers.h b/PyGLM/internal_functions/packers.h
index a6054c1..e51f38d 100644
--- a/PyGLM/internal_functions/packers.h
+++ b/PyGLM/internal_functions/packers.h
@@ -73,25 +73,25 @@ pack_qua(glm::qua<T> value) {
 }
 
 template<typename T>
-static PyObject*
+static inline PyObject*
 pack(T value) {
 	return PyGLM_PyObject_FromNumber(value);
 }
 
 template<int L, typename T>
-static PyObject*
+static inline PyObject*
 pack(glm::vec<L, T> value) {
 	return pack_vec(value);
 }
 
 template<int C, int R, typename T>
-static PyObject*
+static inline PyObject*
 pack(glm::mat<C, R, T> value) {
 	return pack_mat(value);
 }
 
 template<typename T>
-static PyObject*
+static inline PyObject*
 pack(glm::qua<T> value) {
 	return pack_qua(value);
 }
\ No newline at end of file
diff --git a/wiki/function-reference/detail/func_common.md b/wiki/function-reference/detail/func_common.md
index b57567c..b300a68 100644
--- a/wiki/function-reference/detail/func_common.md
+++ b/wiki/function-reference/detail/func_common.md
@@ -229,6 +229,9 @@ It contains common GLSL functions\.
 #### <code>glm.<code>**max**(**a**: *vecN*, **b**: *vecN*, **c**: *vecN*, **d**: *vecN*) -\> *vecN*</code></code>  
 &emsp;&emsp;Returns the maximum component wise value of 4 inputs\.  
   
+#### <code>glm.<code>**max**(**iterable**) -\> *any*</code></code>  
+&emsp;&emsp;Returns the greatest number or the maximum component wise value respectively\.  
+  
 ### min\(\) function  
 #### <code>glm.<code>**min**(**x**: *number*, **y**: *number*) -\> *float*</code></code>  
 &emsp;&emsp;Returns ``` y ``` if ``` y < x ```; otherwise, it returns ``` x ```\.  
@@ -251,6 +254,9 @@ It contains common GLSL functions\.
 #### <code>glm.<code>**min**(**a**: *vecN*, **b**: *vecN*, **c**: *vecN*, **d**: *vecN*) -\> *vecN*</code></code>  
 &emsp;&emsp;Returns the minimum component wise value of 4 inputs\.  
   
+#### <code>glm.<code>**min**(**iterable**) -\> *any*</code></code>  
+&emsp;&emsp;Returns the smallest number or the minimum component wise value respectively\.  
+  
 ### mix\(\) function  
 #### <code>glm.<code>**mix**(**x**: *number*, **y**: *number*, **a**: *float*) -\> *number*</code></code>  
 &emsp;&emsp;Returns ``` x * (1.0 - a) + y * a ```, i\.e\., the linear blend of ``` x ``` and ``` y ``` using the floating\-point  
diff --git a/wiki/function-reference/detail/func_common.sb b/wiki/function-reference/detail/func_common.sb
index 648456a..351a8d3 100644
--- a/wiki/function-reference/detail/func_common.sb
+++ b/wiki/function-reference/detail/func_common.sb
@@ -227,6 +227,9 @@ It contains common GLSL functions.
 \raw\#### <code>glm.<code>**max**(**a**: *vecN*, **b**: *vecN*, **c**: *vecN*, **d**: *vecN*) -\\> *vecN*</code></code>\raw\
 \raw\&emsp;&emsp;\raw\Returns the maximum component wise value of 4 inputs.
 
+\raw\#### <code>glm.<code>**max**(**iterable**) -\\> *any*</code></code>\raw\
+\raw\&emsp;&emsp;\raw\Returns the greatest number or the maximum component wise value respectively.
+
 \h3\min() function\h3\
 \raw\#### <code>glm.<code>**min**(**x**: *number*, **y**: *number*) -\\> *float*</code></code>\raw\
 \raw\&emsp;&emsp;\raw\Returns \code\y\code\ if \code\y < x\code\; otherwise, it returns \code\x\code\.
@@ -249,6 +252,9 @@ It contains common GLSL functions.
 \raw\#### <code>glm.<code>**min**(**a**: *vecN*, **b**: *vecN*, **c**: *vecN*, **d**: *vecN*) -\\> *vecN*</code></code>\raw\
 \raw\&emsp;&emsp;\raw\Returns the minimum component wise value of 4 inputs.
 
+\raw\#### <code>glm.<code>**min**(**iterable**) -\\> *any*</code></code>\raw\
+\raw\&emsp;&emsp;\raw\Returns the smallest number or the minimum component wise value respectively.
+
 \h3\mix() function\h3\
 \raw\#### <code>glm.<code>**mix**(**x**: *number*, **y**: *number*, **a**: *float*) -\\> *number*</code></code>\raw\
 \raw\&emsp;&emsp;\raw\Returns \code\x * (1.0 - a) + y * a\code\, i.e., the linear blend of \code\x\code\ and \code\y\code\ using the floating-point


Test patch:
diff --git a/test/PyGLM_test.py b/test/PyGLM_test.py
index bb572c1..0d83240 100644
--- a/test/PyGLM_test.py
+++ b/test/PyGLM_test.py
@@ -1286,6 +1286,10 @@ def test_func_common():
         fassert(glm.min, args)
         fassert(glm.max, args)
 
+    for args in gen_args("NN_VV_NNN_VVV_NNNN_VVVV_NNNNNNNNNNNN_VVVVVVVVVVVV__fFiqsuIQSU"):
+        fassert(glm.min, (args,))
+        fassert(glm.max, (args,))
+
     for args in gen_args("NN_VV_VN_NNN_VVV_NNNN_VVVV__fF"):
         fassert(glm.fmin, args)
         fassert(glm.fmax, args)