Repository: bab2min/tomotopy (language: python)

Issue:
Fatal Python error: Segmentation fault when calling `HLDAModel.make_doc().get_topics()`
Hi, thank you for your great work!

I have been experimenting with the HLDA model, and whenever I try to get the topics of a document, my Notebook kernel crashes.

System: MacOS 10.15.7, Python 3.9.4

Code:
```
hlda = tp.HLDAModel.load(some_model)
article: list[str] = Article.get(xxxx).doc
doc = hlda.make_doc(article)
doc.get_topics(top_n=10)
```

Stack Trace
```
Crashed Thread:        0  Dispatch queue: com.apple.main-thread

Exception Type:        EXC_BAD_ACCESS (SIGSEGV)
Exception Codes:       KERN_INVALID_ADDRESS at 0x0000000000000000
Exception Note:        EXC_CORPSE_NOTIFY

VM Regions Near 0:
--> 
    __TEXT                 0000000105a9a000-0000000105d12000 [ 2528K] r-x/r-x SM=COW  /Users/USER/*/*.9

Thread 0 Crashed:: Dispatch queue: com.apple.main-thread
0   libsystem_kernel.dylib        	0x00007fff7358233a __pthread_kill + 10
1   libsystem_pthread.dylib       	0x00007fff7363ee60 pthread_kill + 430
2   libsystem_c.dylib             	0x00007fff7349993e raise + 26
3   libsystem_platform.dylib      	0x00007fff736335fd _sigtramp + 29
4   ???                           	000000000000000000 0 + 0
5   _tomotopy_avx2.cpython-39-darwin.so	0x00000001064c5a4e tomoto::TopicModel<Eigen::Rand::ParallelRandomEngineAdaptor<unsigned int, Eigen::Rand::MersenneTwister<long long vector[4], 312, 156, 31, 13043109905998158313ull, 29, 6148914691236517205ull, 17, 8202884508482404352ull, 37, 18444473444759240704ull, 43, 6364136223846793005ull>, 8>, 4ul, tomoto::IHLDAModel, tomoto::HLDAModel<(tomoto::TermWeight)0, Eigen::Rand::ParallelRandomEngineAdaptor<unsigned int, Eigen::Rand::MersenneTwister<long long vector[4], 312, 156, 31, 13043109905998158313ull, 29, 6148914691236517205ull, 17, 8202884508482404352ull, 37, 18444473444759240704ull, 43, 6364136223846793005ull>, 8>, tomoto::IHLDAModel, void, tomoto::DocumentHLDA<(tomoto::TermWeight)0>, tomoto::ModelStateHLDA<(tomoto::TermWeight)0> >, tomoto::DocumentHLDA<(tomoto::TermWeight)0>, tomoto::ModelStateHLDA<(tomoto::TermWeight)0> >::getTopicsByDoc(tomoto::DocumentBase const*, bool) const + 14
6   _tomotopy_avx2.cpython-39-darwin.so	0x00000001064c5a83 tomoto::TopicModel<Eigen::Rand::ParallelRandomEngineAdaptor<unsigned int, Eigen::Rand::MersenneTwister<long long vector[4], 312, 156, 31, 13043109905998158313ull, 29, 6148914691236517205ull, 17, 8202884508482404352ull, 37, 18444473444759240704ull, 43, 6364136223846793005ull>, 8>, 4ul, tomoto::IHLDAModel, tomoto::HLDAModel<(tomoto::TermWeight)0, Eigen::Rand::ParallelRandomEngineAdaptor<unsigned int, Eigen::Rand::MersenneTwister<long long vector[4], 312, 156, 31, 13043109905998158313ull, 29, 6148914691236517205ull, 17, 8202884508482404352ull, 37, 18444473444759240704ull, 43, 6364136223846793005ull>, 8>, tomoto::IHLDAModel, void, tomoto::DocumentHLDA<(tomoto::TermWeight)0>, tomoto::ModelStateHLDA<(tomoto::TermWeight)0> >, tomoto::DocumentHLDA<(tomoto::TermWeight)0>, tomoto::ModelStateHLDA<(tomoto::TermWeight)0> >::getTopicsByDocSorted(tomoto::DocumentBase const*, unsigned long) const + 35
7   _tomotopy_avx2.cpython-39-darwin.so	0x0000000106894f89 Document_getTopics(DocumentObject*, _object*, _object*) + 185
8   python                        	0x0000000105b21a45 cfunction_call + 69
9   python                        	0x0000000105ae0757 _PyObject_MakeTpCall + 375
10  python                        	0x0000000105bc8340 call_function + 624
11  python                        	0x0000000105bc5452 _PyEval_EvalFrameDefault + 28002
12  python                        	0x0000000105bc9134 _PyEval_EvalCode + 2852
13  python                        	0x0000000105bbe620 PyEval_EvalCode + 64
14  python                        	0x0000000105c0e90d pyrun_file + 333
15  python                        	0x0000000105c0c9c9 PyRun_SimpleFileExFlags + 729
16  python                        	0x0000000105c2b973 Py_RunMain + 2067
17  python                        	0x0000000105c2bea3 pymain_main + 403
18  python                        	0x0000000105c2befb Py_BytesMain + 43
19  libdyld.dylib                 	0x00007fff7343acc9 start + 1
```

Please let me know if there is anything else I can do to help with debugging, thank you.

Update 1: `get_topic_dist()`also crashes.


Discussion / hints:
Hi @dennishylau 
Thank you for reporting a bug.
When you run `make_doc()`, it creates a document without any topic assignment. So calling `get_topics()` in this situation will not give you proper result. You need to call `infer()` to estimate the distribution of topics in the doc before calling `get_topics()`.

```python
doc = hlda.make_doc(article)
hlda.infer(doc) # doc should be inferred first
doc.get_topics(top_n=10)
```

I'll fix crashes when calling `get_topics()` or `get_topic_dist()` and add a warning message to call `infer` first.
Hi @bab2min, wow thank you for the prompt reply! That makes perfect sense, what a silly mistake on my end. Closing this issue now, have a nice day!

Fix patch:
diff --git a/README.kr.rst b/README.kr.rst
index 27ddfd8..dfbfd8f 100644
--- a/README.kr.rst
+++ b/README.kr.rst
@@ -305,6 +305,14 @@ tomotopy의 Python3 예제 코드는 https://github.com/bab2min/tomotopy/blob/ma
 
 역사
 -------
+* 0.12.2 (2021-09-06)
+    * `min_cf > 0`, `min_df > 0`나 `rm_top > 0`로 설정된 `tomotopy.HDPModel`에서 `convert_to_lda`를 호출할때 크래시가 발생하는 문제를 해결했습니다.
+    * `tomotopy.Document.get_topics`와 `tomotopy.Document.get_topic_dist`에 `from_pseudo_doc` 인자가 추가되었습니다. 
+      이 인자는 `PTModel`에 대해서만 유효하며, 이를 통해 토픽 분포를 구할 때 가상 문헌을 사용할지 여부를 선택할 수 있습니다.
+    * `tomotopy.PTModel` 생성시 기본 인자값이 변경되었습니다. `p`를 생략시 `k * 10`으로 설정됩니다.
+    * `make_doc`으로 생성한 문헌을 `infer` 없이 사용할 경우 발생하는 크래시를 해결하고 경고 메세지를 추가했습니다.
+    * 내부 C++코드가 clang c++17 환경에서 컴파일에 실패하는 문제를 해결했습니다.
+
 * 0.12.1 (2021-06-20)
     * `tomotopy.LDAModel.set_word_prior()`가 크래시를 발생시키던 문제를 해결했습니다.
     * 이제 `tomotopy.LDAModel.perplexity`와 `tomotopy.LDAModel.ll_per_word`가 TermWeight가 ONE이 아닌 경우에도 정확한 값을 반환합니다.
diff --git a/README.rst b/README.rst
index f15ae6e..f82291d 100644
--- a/README.rst
+++ b/README.rst
@@ -309,6 +309,14 @@ meaning you can use it for any reasonable purpose and remain in complete ownersh
 
 History
 -------
+* 0.12.2 (2021-09-06)
+    * An issue where calling `convert_to_lda` of `tomotopy.HDPModel` with `min_cf > 0`, `min_df > 0` or `rm_top > 0` causes a crash has been fixed.
+    * A new argument `from_pseudo_doc` is added to `tomotopy.Document.get_topics` and `tomotopy.Document.get_topic_dist`.
+      This argument is only valid for documents of `PTModel`, it enables to control a source for computing topic distribution.
+    * A default value for argument `p` of `tomotopy.PTModel` has been changed. The new default value is `k * 10`.
+    * Using documents generated by `make_doc` without calling `infer` doesn't cause a crash anymore, but just print warning messages.
+    * An issue where the internal C++ code isn't compiled at clang c++17 environment has been fixed.
+
 * 0.12.1 (2021-06-20)
     * An issue where `tomotopy.LDAModel.set_word_prior()` causes a crash has been fixed.
     * Now `tomotopy.LDAModel.perplexity` and `tomotopy.LDAModel.ll_per_word` return the accurate value when `TermWeight` is not `ONE`.
diff --git a/document/document_header.html b/document/document_header.html
index 9063efc..3eb06d9 100644
--- a/document/document_header.html
+++ b/document/document_header.html
@@ -10,8 +10,9 @@
 <a class="homelink" rel="home" title="tomotopy Home" href="/tomotopy" style="display:block; font-size:2em; font-weight:bold; color:#555; padding-bottom:.5em; border-bottom:1px solid silver;"> <img src="/tomotopy/tomoto.png" alt="" style="height:1.5em;"> tomotopy </a>
 <a id='lang-en' href="../en/index.html">English</a> <a id='lang-kr' href="../kr/index.html">한국어</a> 
 <div id="version-link">
-<span>v0.12.1</span>
+<span>v0.12.2</span>
 <ul>
+	<li><a href='/tomotopy/v0.12.2/en'>v0.12.2</a></li>
 	<li><a href='/tomotopy/v0.12.1/en'>v0.12.1</a></li>
 	<li><a href='/tomotopy/v0.12.0/en'>v0.12.0</a></li>
 	<li><a href='/tomotopy/v0.11.1/en'>v0.11.1</a></li>
diff --git a/src/python/PyUtils.h b/src/python/PyUtils.h
index 196a210..aec7ad1 100644
--- a/src/python/PyUtils.h
+++ b/src/python/PyUtils.h
@@ -10,55 +10,64 @@
 #include <functional>
 #include <iostream>
 #include <cstring>
+#include <deque>
+#include <future>
 
 #ifdef _DEBUG
-#undef _DEBUG
-#include <Python.h>
-#define _DEBUG
+	#undef _DEBUG
+	#include <Python.h>
+	#define _DEBUG
 #else
-#include <Python.h>
+	#include <Python.h>
 #endif
 
 #include <frameobject.h>
+
+#define USE_NUMPY
+
 #ifdef MAIN_MODULE
 #else
-#define NO_IMPORT_ARRAY
+	#define NO_IMPORT_ARRAY
+#endif
+
+#ifdef USE_NUMPY
+	#define PY_ARRAY_UNIQUE_SYMBOL TOMOTOPY_ARRAY_API
+	#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION
+	#include <numpy/arrayobject.h>
 #endif
-#define PY_ARRAY_UNIQUE_SYMBOL TOMOTOPY_ARRAY_API
-#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION
-#include <numpy/arrayobject.h>
 
 namespace py
 {
-	struct UniqueObj
+	template<class Ty = PyObject>
+	struct UniqueCObj
 	{
-		PyObject* obj = nullptr;
-		explicit UniqueObj(PyObject* _obj = nullptr) : obj(_obj) {}
-		~UniqueObj()
+		Ty* obj = nullptr;
+		explicit UniqueCObj(Ty* _obj = nullptr) : obj(_obj) {}
+		~UniqueCObj()
 		{
 			Py_XDECREF(obj);
 		}
 
-		UniqueObj(const UniqueObj&) = delete;
-		UniqueObj& operator=(const UniqueObj&) = delete;
+		UniqueCObj(const UniqueCObj&) = delete;
+		UniqueCObj& operator=(const UniqueCObj&) = delete;
 
-		UniqueObj(UniqueObj&& o) noexcept
+		UniqueCObj(UniqueCObj&& o) noexcept
 		{
 			std::swap(obj, o.obj);
 		}
 
-		UniqueObj& operator=(UniqueObj&& o) noexcept
+		UniqueCObj& operator=(UniqueCObj&& o) noexcept
 		{
 			std::swap(obj, o.obj);
 			return *this;
 		}
 
-		PyObject* get() const
+		Ty* get() const
 		{
 			return obj;
 		}
 
-		PyObject* release()
+		Ty* release()
 		{
 			auto o = obj;
 			obj = nullptr;
@@ -70,12 +79,85 @@ namespace py
 			return !!obj;
 		}
 
-		operator PyObject*() const
+		operator Ty* () const
+		{
+			return obj;
+		}
+
+		Ty* operator->()
+		{
+			return obj;
+		}
+
+		const Ty* operator->() const
 		{
 			return obj;
 		}
 	};
 
+	template<class Ty = PyObject>
+	struct SharedCObj
+	{
+		Ty* obj = nullptr;
+		explicit SharedCObj(Ty* _obj = nullptr) : obj(_obj) {}
+		~SharedCObj()
+		{
+			Py_XDECREF(obj);
+		}
+
+		SharedCObj(const SharedCObj& o)
+			: obj(o.obj)
+		{
+			Py_INCREF(obj);
+		}
+		SharedCObj& operator=(const SharedCObj& o)
+		{
+			Py_XDECREF(obj);
+			obj = o.obj;
+			Py_INCREF(obj);
+			return *this;
+		}
+
+		SharedCObj(SharedCObj&& o) noexcept
+		{
+			std::swap(obj, o.obj);
+		}
+
+		SharedCObj& operator=(SharedCObj&& o) noexcept
+		{
+			std::swap(obj, o.obj);
+			return *this;
+		}
+
+		Ty* get() const
+		{
+			return obj;
+		}
+
+		operator bool() const
+		{
+			return !!obj;
+		}
+
+		operator Ty* () const
+		{
+			return obj;
+		}
+
+		Ty* operator->()
+		{
+			return obj;
+		}
+
+		const Ty* operator->() const
+		{
+			return obj;
+		}
+	};
+
+	using UniqueObj = UniqueCObj<>;
+	using SharedObj = SharedCObj<>;
+
 	class ExcPropagation : public std::runtime_error
 	{
 	public:
@@ -327,60 +409,22 @@ namespace py
 	};
 
 	template<typename _Fn>
-	auto handleExc(_Fn&& fn) 
-		-> typename std::enable_if<std::is_pointer<decltype(fn())>::value, decltype(fn())>::type
-	{
-		try
-		{
-			return fn();
-		}
-		catch (const ExcPropagation&)
-		{
-		}
-		catch (const BaseException& e)
-		{
-			PyErr_SetString(e.pytype(), e.what());
-		}
-		catch (const std::exception& e)
-		{
-			std::cerr << "Uncaughted c++ exception: " << e.what() << std::endl;
-			PyErr_SetString(PyExc_RuntimeError, e.what());
-		}
-		return nullptr;
-	}
+	auto handleExc(_Fn&& fn)
+		-> typename std::enable_if<std::is_pointer<decltype(fn())>::value, decltype(fn())>::type;
 
 	template<typename _Fn>
 	auto handleExc(_Fn&& fn)
-		-> typename std::enable_if<std::is_integral<decltype(fn())>::value, decltype(fn())>::type
-	{
-		try
-		{
-			return fn();
-		}
-		catch (const ExcPropagation&)
-		{
-		}
-		catch (const BaseException& e)
-		{
-			PyErr_SetString(e.pytype(), e.what());
-		}
-		catch (const std::exception& e)
-		{
-			std::cerr << "Uncaughted c++ exception: " << e.what() << std::endl;
-			PyErr_SetString(PyExc_RuntimeError, e.what());
-		}
-		return -1;
-	}
+		-> typename std::enable_if<std::is_integral<decltype(fn())>::value, decltype(fn())>::type;
 
 	class ConversionFail : public ValueError
 	{
 	public:
 		using ValueError::ValueError;
 
-		template<typename _Ty, 
+		template<typename _Ty,
 			typename = typename std::enable_if<std::is_constructible<std::function<std::string()>, _Ty>::value>::type
 		>
-		ConversionFail(_Ty&& callable) : ValueError{ callable() }
+			ConversionFail(_Ty&& callable) : ValueError{ callable() }
 		{
 		}
 	};
@@ -421,11 +465,11 @@ namespace py
 	inline _Ty toCpp(PyObject* obj)
 	{
 		if (!obj) throw ConversionFail{ "cannot convert null pointer into appropriate C++ type" };
-		return ValueBuilder<_Ty>{}._toCpp(obj, [=](){ return "cannot convert " + repr(obj) + " into appropriate C++ type"; });
+		return ValueBuilder<_Ty>{}._toCpp(obj, [=]() { return "cannot convert " + repr(obj) + " into appropriate C++ type"; });
 	}
 
 	template<typename _Ty>
-	struct ValueBuilder<_Ty, 
+	struct ValueBuilder<_Ty,
 		typename std::enable_if<std::is_integral<_Ty>::value>::type>
 	{
 		PyObject* operator()(_Ty v)
@@ -495,6 +539,63 @@ namespace py
 		}
 	};
 
+	template<>
+	struct ValueBuilder<std::u16string>
+	{
+		PyObject* operator()(const std::u16string& v)
+		{
+			return PyUnicode_DecodeUTF16((const char*)v.data(), v.size() * 2, nullptr, nullptr);
+		}
+
+		template<typename _FailMsg>
+		std::u16string _toCpp(PyObject* obj, _FailMsg&& msg)
+		{
+			UniqueObj uobj{ PyUnicode_FromObject(obj) };
+			if (!uobj) throw ConversionFail{ std::forward<_FailMsg>(msg) };
+			size_t len = PyUnicode_GET_LENGTH(uobj.get());
+			std::u16string ret;
+			switch (PyUnicode_KIND(uobj.get()))
+			{
+			case PyUnicode_1BYTE_KIND:
+			{
+				auto* p = PyUnicode_1BYTE_DATA(uobj.get());
+				ret.resize(len);
+				std::copy(p, p + len, &ret[0]);
+				break;
+			}
+			case PyUnicode_2BYTE_KIND:
+			{
+				auto* p = PyUnicode_2BYTE_DATA(uobj.get());
+				ret.resize(len);
+				std::copy(p, p + len, &ret[0]);
+				break;
+			}
+			case PyUnicode_4BYTE_KIND:
+			{
+				auto* p = PyUnicode_4BYTE_DATA(uobj.get());
+				for (size_t i = 0; i < len; ++i)
+				{
+					auto c = p[i];
+					ret.reserve(len);
+					if (c < 0x10000)
+					{
+						ret.push_back(c);
+					}
+					else
+					{
+						ret.push_back(0xD800 - (0x10000 >> 10) + (c >> 10));
+						ret.push_back(0xDC00 + (c & 0x3FF));
+					}
+				}
+				break;
+			}
+			default:
+				throw ConversionFail{ std::forward<_FailMsg>(msg) };
+			}
+			return ret;
+		}
+	};
+
 	template<>
 	struct ValueBuilder<const char*>
 	{
@@ -512,6 +613,15 @@ namespace py
 		}
 	};
 
+	template<size_t len>
+	struct ValueBuilder<char[len]>
+	{
+		PyObject* operator()(const char(&v)[len])
+		{
+			return PyUnicode_FromStringAndSize(v, len - 1);
+		}
+	};
+
 	template<>
 	struct ValueBuilder<bool>
 	{
@@ -551,15 +661,61 @@ namespace py
 		}
 	};
 
-	template<>
-	struct ValueBuilder<UniqueObj>
+	template<typename Ty>
+	struct ValueBuilder<UniqueCObj<Ty>>
 	{
-		PyObject* operator()(UniqueObj&& v)
+		PyObject* operator()(UniqueCObj<Ty>&& v)
 		{
 			if (v)
 			{
 				Py_INCREF(v);
-				return v;
+				return (PyObject*)v.get();
+			}
+			else
+			{
+				Py_INCREF(Py_None);
+				return Py_None;
+			}
+		}
+
+		PyObject* operator()(const UniqueCObj<Ty>& v)
+		{
+			if (v)
+			{
+				Py_INCREF(v);
+				return (PyObject*)v.get();
+			}
+			else
+			{
+				Py_INCREF(Py_None);
+				return Py_None;
+			}
+		}
+	};
+
+	template<typename Ty>
+	struct ValueBuilder<SharedCObj<Ty>>
+	{
+		PyObject* operator()(SharedCObj<Ty>&& v)
+		{
+			if (v)
+			{
+				Py_INCREF(v);
+				return (PyObject*)v.get();
+			}
+			else
+			{
+				Py_INCREF(Py_None);
+				return Py_None;
+			}
+		}
+
+		PyObject* operator()(const SharedCObj<Ty>& v)
+		{
+			if (v)
+			{
+				Py_INCREF(v);
+				return (PyObject*)v.get();
 			}
 			else
 			{
@@ -601,7 +757,7 @@ namespace py
 			for (auto& p : v)
 			{
 				py::UniqueObj key{ buildPyValue(p.first) }, val{ buildPyValue(p.second) };
-				if(PyDict_SetItem(ret, key, val)) return nullptr;
+				if (PyDict_SetItem(ret, key, val)) return nullptr;
 			}
 			return ret;
 		}
@@ -620,6 +776,7 @@ namespace py
 		}
 	};
 
+#ifdef USE_NUMPY
 	namespace detail
 	{
 		template<typename _Ty>
@@ -633,7 +790,7 @@ namespace py
 		template<>
 		struct NpyType<int8_t>
 		{
-			enum { 
+			enum {
 				type = NPY_INT8,
 				signed_type = type,
 				npy_type = type,
@@ -643,7 +800,7 @@ namespace py
 		template<>
 		struct NpyType<uint8_t>
 		{
-			enum { 
+			enum {
 				type = NPY_UINT8,
 				signed_type = NPY_INT8,
 				npy_type = type,
@@ -653,7 +810,7 @@ namespace py
 		template<>
 		struct NpyType<int16_t>
 		{
-			enum { 
+			enum {
 				type = NPY_INT16,
 				signed_type = type,
 				npy_type = type,
@@ -663,7 +820,7 @@ namespace py
 		template<>
 		struct NpyType<uint16_t>
 		{
-			enum { 
+			enum {
 				type = NPY_UINT16,
 				signed_type = NPY_INT16,
 				npy_type = type,
@@ -673,7 +830,7 @@ namespace py
 		template<>
 		struct NpyType<int32_t>
 		{
-			enum { 
+			enum {
 				type = NPY_INT32,
 				signed_type = type,
 				npy_type = type,
@@ -683,7 +840,7 @@ namespace py
 		template<>
 		struct NpyType<uint32_t>
 		{
-			enum { 
+			enum {
 				type = NPY_UINT32,
 				signed_type = NPY_INT32,
 				npy_type = type,
@@ -693,7 +850,7 @@ namespace py
 		template<>
 		struct NpyType<int64_t>
 		{
-			enum { 
+			enum {
 				type = NPY_INT64,
 				signed_type = type,
 				npy_type = type,
@@ -703,8 +860,8 @@ namespace py
 		template<>
 		struct NpyType<uint64_t>
 		{
-			enum { 
-				type = NPY_UINT64, 
+			enum {
+				type = NPY_UINT64,
 				signed_type = NPY_INT64,
 				npy_type = type,
 			};
@@ -713,8 +870,8 @@ namespace py
 		template<>
 		struct NpyType<float>
 		{
-			enum { 
-				type = NPY_FLOAT, 
+			enum {
+				type = NPY_FLOAT,
 				signed_type = type,
 				npy_type = type,
 			};
@@ -723,8 +880,8 @@ namespace py
 		template<>
 		struct NpyType<double>
 		{
-			enum { 
-				type = NPY_DOUBLE, 
+			enum {
+				type = NPY_DOUBLE,
 				signed_type = type,
 				npy_type = type,
 			};
@@ -734,12 +891,20 @@ namespace py
 	struct cast_to_signed_t {};
 	static constexpr cast_to_signed_t cast_to_signed{};
 
+	template<typename _Ty>
+	struct numpy_able : std::integral_constant<bool, std::is_arithmetic<_Ty>::value> {};
+
+#else
+	template<typename _Ty>
+	struct numpy_able : std::false_type {};
+#endif
 	struct force_list_t {};
 	static constexpr force_list_t force_list{};
 
+#ifdef USE_NUMPY
 	template<typename _Ty>
-	struct ValueBuilder<std::vector<_Ty>, 
-		typename std::enable_if<std::is_arithmetic<_Ty>::value>::type>
+	struct ValueBuilder<std::vector<_Ty>,
+		typename std::enable_if<numpy_able<_Ty>::value>::type>
 	{
 		PyObject* operator()(const std::vector<_Ty>& v)
 		{
@@ -774,10 +939,11 @@ namespace py
 			}
 		}
 	};
+#endif
 
 	template<typename _Ty>
 	struct ValueBuilder<std::vector<_Ty>,
-		typename std::enable_if<!std::is_arithmetic<_Ty>::value>::type>
+		typename std::enable_if<!numpy_able<_Ty>::value>::type>
 	{
 		PyObject* operator()(const std::vector<_Ty>& v)
 		{
@@ -856,8 +1022,9 @@ namespace py
 		}
 	}
 
+#ifdef USE_NUMPY
 	template<typename _Ty>
-	inline typename std::enable_if<std::is_arithmetic<_Ty>::value, PyObject*>::type
+	inline typename std::enable_if<numpy_able<_Ty>::value, PyObject*>::type
 		buildPyValue(const std::vector<_Ty>& v, cast_to_signed_t)
 	{
 		npy_intp size = v.size();
@@ -865,10 +1032,11 @@ namespace py
 		std::memcpy(PyArray_DATA((PyArrayObject*)obj), v.data(), sizeof(_Ty) * size);
 		return obj;
 	}
+#endif
 
 	template<typename _Ty>
 	inline typename std::enable_if<
-		!std::is_arithmetic<typename std::iterator_traits<_Ty>::value_type>::value,
+		!numpy_able<typename std::iterator_traits<_Ty>::value_type>::value,
 		PyObject*
 	>::type buildPyValue(_Ty first, _Ty last)
 	{
@@ -894,7 +1062,7 @@ namespace py
 
 	template<typename _Ty, typename _Tx>
 	inline typename std::enable_if<
-		!std::is_arithmetic<
+		!numpy_able<
 			typename std::result_of<_Tx(typename std::iterator_traits<_Ty>::value_type)>::type
 		>::value,
 		PyObject*
@@ -909,10 +1077,10 @@ namespace py
 		return ret;
 	}
 
-
+#ifdef USE_NUMPY
 	template<typename _Ty>
 	inline typename std::enable_if<
-		std::is_arithmetic<typename std::iterator_traits<_Ty>::value_type>::value,
+		numpy_able<typename std::iterator_traits<_Ty>::value_type>::value,
 		PyObject*
 	>::type buildPyValue(_Ty first, _Ty last)
 	{
@@ -929,7 +1097,7 @@ namespace py
 
 	template<typename _Ty, typename _Tx>
 	inline typename std::enable_if<
-		std::is_arithmetic<
+		numpy_able<
 			typename std::result_of<_Tx(typename std::iterator_traits<_Ty>::value_type)>::type
 		>::value,
 		PyObject*
@@ -945,6 +1113,7 @@ namespace py
 		}
 		return ret;
 	}
+#endif
 
 	namespace detail
 	{
@@ -983,7 +1152,7 @@ namespace py
 		template<typename _Ty, typename... _Rest>
 		inline void setDictItemSkipNull(PyObject* dict, const char** keys, _Ty&& value, _Rest&& ... rest)
 		{
-			if(!isNull(value))
+			if (!isNull(value))
 			{
 				UniqueObj v{ buildPyValue(value) };
 				PyDict_SetItemString(dict, keys[0], v);
@@ -1034,4 +1203,180 @@ namespace py
 		detail::setTupleItem<0>(tuple, std::forward<_Rest>(rest)...);
 		return tuple;
 	}
+
+	template<class Derived>
+	struct CObject
+	{
+		PyObject_HEAD;
+
+		static PyObject* _new(PyTypeObject* subtype, PyObject* args, PyObject* kwargs)
+		{
+			return handleExc([&]()
+			{
+				py::UniqueObj ret{ subtype->tp_alloc(subtype, 0) };
+				new ((Derived*)ret.get()) Derived;
+				return ret.release();
+			});
+		}
+
+		static void dealloc(Derived* self)
+		{
+			self->~Derived();
+			Py_TYPE(self)->tp_free((PyObject*)self);
+		}
+	};
+
+	template<class Derived, class RetTy>
+	struct ResultIter : public CObject<Derived>
+	{
+		UniqueObj inputIter;
+		std::deque<std::future<RetTy>> futures;
+		std::deque<SharedObj> inputItems;
+		bool echo = false;
+
+		~ResultIter()
+		{
+			for (auto& p : futures)
+			{
+				p.get();
+			}
+		}
+
+		static int init(Derived* self, PyObject*, PyObject*)
+		{
+			return 0;
+		}
+
+		static Derived* iter(Derived* self)
+		{
+			Py_INCREF(self);
+			return self;
+		}
+
+		static PyObject* iternext(Derived* self)
+		{
+			return handleExc([&]() -> PyObject* {
+				if (!self->feed() && self->futures.empty()) return nullptr;
+				auto f = std::move(self->futures.front());
+				self->futures.pop_front();
+				if (self->echo)
+				{
+					auto input = std::move(self->inputItems.front());
+					self->inputItems.pop_front();
+					return buildPyTuple(UniqueObj{ self->buildPy(f.get()) }, input);
+				}
+				else
+				{
+					return self->buildPy(f.get());
+				}
+			});
+		}
+
+		bool feed()
+		{
+			SharedObj item{ PyIter_Next(inputIter) };
+			if (!item)
+			{
+				if (PyErr_Occurred()) throw ExcPropagation{};
+				return false;
+			}
+			if (echo) inputItems.emplace_back(item);
+			futures.emplace_back(static_cast<Derived*>(this)->feedNext(std::move(item)));
+			return true;
+		}
+
+		std::future<RetTy> feedNext(py::SharedObj&& next)
+		{
+			return {};
+		}
+
+		PyObject* buildPy(RetTy&& v)
+		{
+			return py::buildPyValue(std::move(v));
+		}
+	};
+
+	template<typename _Fn>
+	auto handleExc(_Fn&& fn)
+		-> typename std::enable_if<std::is_pointer<decltype(fn())>::value, decltype(fn())>::type
+	{
+		try
+		{
+			return fn();
+		}
+		catch (const ExcPropagation&)
+		{
+		}
+		catch (const BaseException& e)
+		{
+			if (PyErr_Occurred())
+			{
+				PyObject* exc, * val, * tb, * val2;
+				PyErr_Fetch(&exc, &val, &tb);
+				if (tb)
+				{
+					PyException_SetTraceback(val, tb);
+					Py_DECREF(tb);
+				}
+				Py_DECREF(exc);
+				PyObject* et = e.pytype();
+				val2 = PyObject_CallFunctionObjArgs(et, py::UniqueObj{ buildPyValue(e.what()) }.get(), nullptr);
+				PyException_SetCause(val2, val);
+				PyErr_SetObject(et, val2);
+				Py_DECREF(val2);
+			}
+			else
+			{
+				PyErr_SetString(e.pytype(), e.what());
+			}
+		}
+		catch (const std::exception& e)
+		{
+			std::cerr << "Uncaughted c++ exception: " << e.what() << std::endl;
+			PyErr_SetString(PyExc_RuntimeError, e.what());
+		}
+		return nullptr;
+	}
+
+	template<typename _Fn>
+	auto handleExc(_Fn&& fn)
+		-> typename std::enable_if<std::is_integral<decltype(fn())>::value, decltype(fn())>::type
+	{
+		try
+		{
+			return fn();
+		}
+		catch (const ExcPropagation&)
+		{
+		}
+		catch (const BaseException& e)
+		{
+			if (PyErr_Occurred())
+			{
+				PyObject* exc, * val, * tb, * val2;
+				PyErr_Fetch(&exc, &val, &tb);
+				if (tb)
+				{
+					PyException_SetTraceback(val, tb);
+					Py_DECREF(tb);
+				}
+				Py_DECREF(exc);
+				PyObject* et = e.pytype();
+				val2 = PyObject_CallFunctionObjArgs(et, py::UniqueObj{ buildPyValue(e.what()) }.get(), nullptr);
+				PyException_SetCause(val2, val);
+				PyErr_SetObject(et, val2);
+				Py_DECREF(val2);
+			}
+			else
+			{
+				PyErr_SetString(e.pytype(), e.what());
+			}
+		}
+		catch (const std::exception& e)
+		{
+			std::cerr << "Uncaughted c++ exception: " << e.what() << std::endl;
+			PyErr_SetString(PyExc_RuntimeError, e.what());
+		}
+		return -1;
+	}
 }
diff --git a/src/python/docs.h b/src/python/docs.h
index f674a43..c269ce1 100644
--- a/src/python/docs.h
+++ b/src/python/docs.h
@@ -22,12 +22,32 @@ An instance of this type can be acquired from `tomotopy.LDAModel.make_doc` metho
 u8R""(이 타입은 토픽 모델에 사용되는 문헌들에 접근할 수 있는 추상 인터페이스을 제공합니다.)"");
 
 DOC_SIGNATURE_EN_KO(Document_get_topics__doc__,
-    "get_topics(self, top_n=10)",
-    u8R""(Return the `top_n` topics with its probability of the document.)"",
-    u8R""(현재 문헌의 상위 `top_n`개의 토픽과 그 확률을 `tuple`의 `list` 형태로 반환합니다.)"");
+    "get_topics(self, top_n=10, from_pseudo_doc=False)",
+    u8R""(Return the `top_n` topics with its probability of the document.
+
+Parameters
+----------
+top_n : int
+    the `n` in "top-n"
+from_pseudo_doc : bool
+    .. versionadded:: 0.12.2
+
+    If True, it returns the topic distribution of its pseudo document. Only valid for `tomotopy.PTModel`.
+)"",
+    u8R""(현재 문헌의 상위 `top_n`개의 토픽과 그 확률을 `tuple`의 `list` 형태로 반환합니다.
+    
+Parameters
+----------
+top_n : int
+    "상위-n"에서 n의 값
+from_pseudo_doc : bool
+    .. versionadded:: 0.12.2
+
+    참일 경우 가상 문헌의 토픽 분포를 반환합니다. `tomotopy.PTModel`에서만 유효합니다.
+)"");
 
 DOC_SIGNATURE_EN_KO(Document_get_topic_dist__doc__,
-    "get_topic_dist(self, normalize=True)",
+    "get_topic_dist(self, normalize=True, from_pseudo_doc=False)",
     u8R""(Return a distribution of the topics in the document.
 
 Parameters
@@ -36,6 +56,10 @@ normalize : bool
     .. versionadded:: 0.11.0
 
     If True, it returns the probability distribution with the sum being 1. Otherwise it returns the distribution of raw values.
+from_pseudo_doc : bool
+    .. versionadded:: 0.12.2
+
+    If True, it returns the topic distribution of its pseudo document. Only valid for `tomotopy.PTModel`.
 )"",
 u8R""(현재 문헌의 토픽 확률 분포를 `list` 형태로 반환합니다.
 
@@ -45,6 +69,10 @@ normalize : bool
     .. versionadded:: 0.11.0
 
     참일 경우 총합이 1이 되는 확률 분포를 반환하고, 거짓일 경우 정규화되지 않는 값을 그대로 반환합니다.
+from_pseudo_doc : bool
+    .. versionadded:: 0.12.2
+
+    참일 경우 가상 문헌의 토픽 분포를 반환합니다. `tomotopy.PTModel`에서만 유효합니다.
 )"");
 
 DOC_SIGNATURE_EN_KO(Document_get_sub_topics__doc__,
@@ -3436,7 +3464,7 @@ u8R""(이 프로퍼티는 `DTModel`에서 사용불가합니다. 대신 `DTModel
 */
 
 DOC_SIGNATURE_EN_KO(PT___init____doc__,
-    "PTModel(tw=TermWeight.ONE, min_cf=0, min_df=0, rm_top=0, k=1, p=1, alpha=0.1, eta=0.01, seed=None, corpus=None, transform=None)",
+    "PTModel(tw=TermWeight.ONE, min_cf=0, min_df=0, rm_top=0, k=1, p=None, alpha=0.1, eta=0.01, seed=None, corpus=None, transform=None)",
     u8R""(.. versionadded:: 0.11.0
 This type provides Pseudo-document based Topic Model (PTM) and its implementation is based on following papers:
 	
@@ -3459,6 +3487,8 @@ k : int
     the number of topics between 1 ~ 32767
 p : int
     the number of pseudo documents
+    ..versionchanged:: 0.12.2
+        The default value is changed to `10 * k`.
 alpha : Union[float, Iterable[float]]
     hyperparameter of Dirichlet distribution for document-topic, given as a single `float` in case of symmetric prior and as a list with length `k` of `float` in case of asymmetric prior.
 eta : float
@@ -3492,6 +3522,8 @@ k : int
     토픽의 개수, 1 ~ 32767 사이의 정수
 p : int
     가상 문헌의 개수
+    ..versionchanged:: 0.12.2
+        기본값이 `10 * k`로 변경되었습니다.
 alpha : Union[float, Iterable[float]]
     문헌-토픽 디리클레 분포의 하이퍼 파라미터, 대칭일 경우 `float`값 하나로, 비대칭일 경우 `k` 길이의 `float` 리스트로 입력할 수 있습니다.
 eta : float
diff --git a/src/python/py_LDA.cpp b/src/python/py_LDA.cpp
index 2809976..53045c2 100644
--- a/src/python/py_LDA.cpp
+++ b/src/python/py_LDA.cpp
@@ -216,6 +216,7 @@ PyObject* LDA_infer(TopicModelObject* self, PyObject* args, PyObject *kwargs)
 				std::vector<tomoto::DocumentBase*> docs;
 				docs.emplace_back((tomoto::DocumentBase*)doc->getBoundDoc());
 				float ll = self->inst->infer(docs, iteration, tolerance, workers, (tomoto::ParallelScheme)ps, !!together)[0];
+				doc->initialized = true;
 				return Py_BuildValue("(Nf)", py::buildPyValue(self->inst->getTopicsByDoc(doc->getBoundDoc())), ll);
 			}
 			else
@@ -226,6 +227,7 @@ PyObject* LDA_infer(TopicModelObject* self, PyObject* args, PyObject *kwargs)
 		else if ((iter = py::UniqueObj{ PyObject_GetIter(argDoc) }) != nullptr)
 		{
 			std::vector<tomoto::DocumentBase*> docs;
+			std::vector<DocumentObject*> docObjs;
 			py::UniqueObj item;
 			while ((item = py::UniqueObj{ PyIter_Next(iter) }))
 			{
@@ -233,9 +235,13 @@ PyObject* LDA_infer(TopicModelObject* self, PyObject* args, PyObject *kwargs)
 				auto* doc = (DocumentObject*)item.get();
 				if (doc->corpus->tm != self) throw py::ValueError{ "`doc` was from another model, not fit to this model" };
 				docs.emplace_back((tomoto::DocumentBase*)doc->doc);
+				docObjs.emplace_back(doc);
 			}
 			if (PyErr_Occurred()) throw py::ExcPropagation{};
 			auto ll = self->inst->infer(docs, iteration, tolerance, workers, (tomoto::ParallelScheme)ps, !!together);
+			
+			for (auto doc : docObjs) doc->initialized = true;
+
 			PyObject* ret = PyList_New(docs.size());
 			size_t i = 0;
 			for (auto d : docs)
diff --git a/src/python/py_PA.cpp b/src/python/py_PA.cpp
index b00fac8..f0d75b3 100644
--- a/src/python/py_PA.cpp
+++ b/src/python/py_PA.cpp
@@ -165,6 +165,7 @@ static PyObject* PA_infer(TopicModelObject* self, PyObject* args, PyObject *kwar
 				std::vector<tomoto::DocumentBase*> docs;
 				docs.emplace_back((tomoto::DocumentBase*)doc->doc);
 				double ll = self->inst->infer(docs, iteration, tolerance, workers, (tomoto::ParallelScheme)ps, !!together)[0];
+				doc->initialized = true;
 				return Py_BuildValue("((NN)f)", py::buildPyValue(inst->getTopicsByDoc(doc->getBoundDoc())),
 					py::buildPyValue(inst->getSubTopicsByDoc(doc->getBoundDoc())), ll);
 			}
@@ -177,6 +178,7 @@ static PyObject* PA_infer(TopicModelObject* self, PyObject* args, PyObject *kwar
 		else if ((iter = py::UniqueObj{ PyObject_GetIter(argDoc) }) != nullptr)
 		{
 			std::vector<tomoto::DocumentBase*> docs;
+			std::vector<DocumentObject*> docObjs;
 			py::UniqueObj item;
 			while ((item = py::UniqueObj{ PyIter_Next(iter) }))
 			{
@@ -184,10 +186,14 @@ static PyObject* PA_infer(TopicModelObject* self, PyObject* args, PyObject *kwar
 				auto* doc = (DocumentObject*)item.get();
 				if (doc->corpus->tm != self) throw py::ValueError{ "`doc` was from another model, not fit to this model" };
 				docs.emplace_back((tomoto::DocumentBase*)doc->getBoundDoc());
+				docObjs.emplace_back(doc);
 			}
 			if (PyErr_Occurred()) throw py::ExcPropagation{};
 			if (!self->isPrepared) throw py::RuntimeError{ "cannot infer with untrained model" };
 			auto ll = inst->infer(docs, iteration, tolerance, workers, (tomoto::ParallelScheme)ps, !!together);
+
+			for (auto doc : docObjs) doc->initialized = true;
+
 			PyObject* ret = PyList_New(docs.size());
 			size_t i = 0;
 			for (auto d : docs)
diff --git a/src/python/py_utils.cpp b/src/python/py_utils.cpp
index 8181c0b..64321fc 100644
--- a/src/python/py_utils.cpp
+++ b/src/python/py_utils.cpp
@@ -1097,11 +1097,17 @@ static PyObject* Document_getTopics(DocumentObject* self, PyObject* args, PyObje
 	size_t fromPseudoDoc = 0;
 	static const char* kwlist[] = { "top_n", "from_pseudo_doc", nullptr};
 	if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|np", (char**)kwlist, &topN, &fromPseudoDoc)) return nullptr;
-	return py::handleExc([&]()
+	return py::handleExc([&]() -> PyObject*
 	{
 		if (self->corpus->isIndependent()) throw py::RuntimeError{ "This method can only be called by documents bound to the topic model." };
 		if (!self->corpus->tm->inst) throw py::RuntimeError{ "inst is null" };
 		if (!self->corpus->tm->isPrepared) throw py::RuntimeError{ "train() should be called first for calculating the topic distribution" };
+
+		if (self->owner && !self->initialized)
+		{
+			if (PyErr_WarnEx(PyExc_RuntimeWarning, "This document has no topic information. Call `infer()` method passing this document as an argument first!", 1)) return nullptr;
+		}
+
 #ifdef TM_PT
 		if (fromPseudoDoc) return Document_getTopicsFromPseudoDoc(self, topN);
 #endif
@@ -1115,11 +1121,16 @@ static PyObject* Document_getTopicDist(DocumentObject* self, PyObject* args, PyO
 	size_t fromPseudoDoc = 0;
 	static const char* kwlist[] = { "normalize", "from_pseudo_doc", nullptr };
 	if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|p", (char**)kwlist, &normalize)) return nullptr;
-	return py::handleExc([&]()
+	return py::handleExc([&]() -> PyObject*
 	{
 		if (self->corpus->isIndependent()) throw py::RuntimeError{ "This method can only be called by documents bound to the topic model." };
 		if (!self->corpus->tm->inst) throw py::RuntimeError{ "inst is null" };
 		if (!self->corpus->tm->isPrepared) throw py::RuntimeError{ "train() should be called first for calculating the topic distribution" };
+
+		if (self->owner && !self->initialized)
+		{
+			if (PyErr_WarnEx(PyExc_RuntimeWarning, "This document has no topic information. Call `infer()` method passing this document as an argument first!", 1)) return nullptr;
+		}
 #ifdef TM_PT
 		if (fromPseudoDoc) return Document_getTopicDistFromPseudoDoc(self, !!normalize);
 #endif
diff --git a/src/python/utils.h b/src/python/utils.h
index 901e5fa..f35c874 100644
--- a/src/python/utils.h
+++ b/src/python/utils.h
@@ -158,6 +158,7 @@ struct DocumentObject
 	const tomoto::RawDocKernel* doc;
 	CorpusObject* corpus;
 	bool owner;
+	bool initialized;
 
 	inline const tomoto::RawDoc* getRawDoc() const { return (const tomoto::RawDoc*)doc; }
 	inline const tomoto::DocumentBase* getBoundDoc() const { return (const tomoto::DocumentBase*)doc; }
diff --git a/tomotopy/documentation.kr.rst b/tomotopy/documentation.kr.rst
index 90a2d81..0ab1c42 100644
--- a/tomotopy/documentation.kr.rst
+++ b/tomotopy/documentation.kr.rst
@@ -386,6 +386,14 @@ tomotopy의 Python3 예제 코드는 https://github.com/bab2min/tomotopy/blob/ma
 
 역사
 -------
+* 0.12.2 (2021-09-06)
+    * `min_cf > 0`, `min_df > 0`나 `rm_top > 0`로 설정된 `tomotopy.HDPModel`에서 `convert_to_lda`를 호출할때 크래시가 발생하는 문제를 해결했습니다.
+    * `tomotopy.Document.get_topics`와 `tomotopy.Document.get_topic_dist`에 `from_pseudo_doc` 인자가 추가되었습니다. 
+      이 인자는 `PTModel`에 대해서만 유효하며, 이를 통해 토픽 분포를 구할 때 가상 문헌을 사용할지 여부를 선택할 수 있습니다.
+    * `tomotopy.PTModel` 생성시 기본 인자값이 변경되었습니다. `p`를 생략시 `k * 10`으로 설정됩니다.
+    * `make_doc`으로 생성한 문헌을 `infer` 없이 사용할 경우 발생하는 크래시를 해결하고 경고 메세지를 추가했습니다.
+    * 내부 C++코드가 clang c++17 환경에서 컴파일에 실패하는 문제를 해결했습니다.
+
 * 0.12.1 (2021-06-20)
     * `tomotopy.LDAModel.set_word_prior()`가 크래시를 발생시키던 문제를 해결했습니다.
     * 이제 `tomotopy.LDAModel.perplexity`와 `tomotopy.LDAModel.ll_per_word`가 TermWeight가 ONE이 아닌 경우에도 정확한 값을 반환합니다.
diff --git a/tomotopy/documentation.rst b/tomotopy/documentation.rst
index 334c3da..4bda2ff 100644
--- a/tomotopy/documentation.rst
+++ b/tomotopy/documentation.rst
@@ -389,6 +389,14 @@ meaning you can use it for any reasonable purpose and remain in complete ownersh
 
 History
 -------
+* 0.12.2 (2021-09-06)
+    * An issue where calling `convert_to_lda` of `tomotopy.HDPModel` with `min_cf > 0`, `min_df > 0` or `rm_top > 0` causes a crash has been fixed.
+    * A new argument `from_pseudo_doc` is added to `tomotopy.Document.get_topics` and `tomotopy.Document.get_topic_dist`.
+      This argument is only valid for documents of `PTModel`, it enables to control a source for computing topic distribution.
+    * A default value for argument `p` of `tomotopy.PTModel` has been changed. The new default value is `k * 10`.
+    * Using documents generated by `make_doc` without calling `infer` doesn't cause a crash anymore, but just print warning messages.
+    * An issue where the internal C++ code isn't compiled at clang c++17 environment has been fixed.
+
 * 0.12.1 (2021-06-20)
     * An issue where `tomotopy.LDAModel.set_word_prior()` causes a crash has been fixed.
     * Now `tomotopy.LDAModel.perplexity` and `tomotopy.LDAModel.ll_per_word` return the accurate value when `TermWeight` is not `ONE`.


Test patch:
diff --git a/test/unit_test.py b/test/unit_test.py
index f9b865b..2aa39b9 100644
--- a/test/unit_test.py
+++ b/test/unit_test.py
@@ -296,30 +296,53 @@ def train_multi_corpus(cls, inputFile, mdFields, f, kargs, ps):
     for d in tcorpus2[:10]: print(d.get_ll())
 
 def uninit_doc(cls, inputFile, mdFields, f, kargs, ps):
+    import warnings
+
     print('Test uninitialized doc')
     tw = 0
     print('Initialize model %s with TW=%s ...' % (str(cls), ['one', 'idf', 'pmi'][tw]))
     mdl = cls(tw=tw, min_df=2, rm_top=2, **kargs)
     print('Adding docs...')
-    unseen_docs = []
+    unseen_doc = None
     for n, line in enumerate(open(inputFile, encoding='utf-8')):
         ch = line.strip().split()
         if len(ch) < mdFields + 1: continue
-        if n < 20: unseen_docs.append(line)
+        if n < 1: unseen_doc = line
         else:
             if mdFields:
                 mdl.add_doc(ch[mdFields:], f(ch[:mdFields]))
             else:
                 mdl.add_doc(ch)
+    
     mdl.train(20, parallel=ps)
-    for n, line in enumerate(unseen_docs):
-        ch = line.strip().split()
-        if mdFields:
-            unseen_docs[n] = mdl.make_doc(ch[mdFields:], f(ch[:mdFields]))
-        else:
-            unseen_docs[n] = mdl.make_doc(ch)
-        unseen_docs[n].get_topics()
-        unseen_docs[n].get_topic_dist()
+    ch = unseen_doc.strip().split()
+    if mdFields:
+        unseen_doc = mdl.make_doc(ch[mdFields:], f(ch[:mdFields]))
+    else:
+        unseen_doc = mdl.make_doc(ch)
+    
+    with warnings.catch_warnings():
+        warnings.simplefilter("error")
+        raised_warning = False
+        try:
+            unseen_doc.get_topics()
+            unseen_doc.get_topic_dist()
+        except Warning:
+            raised_warning = True
+        assert raised_warning
+    
+    mdl.infer(unseen_doc)
+
+    with warnings.catch_warnings():
+        warnings.simplefilter("error")
+        raised_warning = False
+        try:
+            unseen_doc.get_topics()
+            unseen_doc.get_topic_dist()
+        except Warning:
+            raised_warning = True
+        assert not raised_warning
+
 
 def test_empty_uid():
     cps = tp.utils.Corpus()