Skip to content

Commit f7d993d

Browse files
committed
Merge remote-tracking branch 'dept/domdev'
2 parents 6f0dcf9 + 40b283b commit f7d993d

File tree

5 files changed

+299
-15
lines changed

5 files changed

+299
-15
lines changed

core/packages/CMakeLists.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,9 @@ set(PACKAGES
3535
)
3636

3737
set(COMPILED_PACKAGES
38-
core/_component.cc)
38+
core/_component.cc
39+
utils/_algorithm.cc
40+
)
3941

4042
#---------------------------------------------------------------------------
4143
# Install packages
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
#include <pybind11/pybind11.h>
2+
#include "pythoncdb/py_algorithms.hh"
3+
#include "pythoncdb/py_kernel.hh"
4+
#include "Algorithm.hh"
5+
6+
using namespace cadabra;
7+
8+
// Virtual base class which uses ExNode instead of Ex::iterator so that
9+
// it can be extended in Python
10+
class PyAlgorithm : public Algorithm
11+
{
12+
public:
13+
PyAlgorithm(Ex_ptr ex)
14+
: Algorithm(*get_kernel_from_scope(), *ex)
15+
, ex(ex)
16+
{
17+
18+
}
19+
20+
result_t py_apply_generic(bool deep, bool repeat, unsigned int depth)
21+
{
22+
return apply_generic(deep, repeat, depth);
23+
}
24+
25+
result_t py_apply_pre_order(bool repeat)
26+
{
27+
return apply_pre_order(repeat);
28+
}
29+
30+
virtual bool py_can_apply(ExNode node) = 0;
31+
32+
bool can_apply(Ex::iterator it) override
33+
{
34+
ExNode node(*get_kernel_from_scope(), ex);
35+
node.ex = ex;
36+
node.topit = it;
37+
node.it = it;
38+
node.stopit = it;
39+
node.stopit.skip_children();
40+
++node.stopit;
41+
node.update(true);
42+
43+
return py_can_apply(node);
44+
}
45+
46+
virtual Algorithm::result_t py_apply(ExNode node) = 0;
47+
48+
Algorithm::result_t apply(Ex::iterator& it) override
49+
{
50+
ExNode node(*get_kernel_from_scope(), ex);
51+
node.ex = ex;
52+
node.topit = it;
53+
node.it = it;
54+
node.stopit = it;
55+
node.stopit.skip_children();
56+
++node.stopit;
57+
node.update(true);
58+
59+
auto res = py_apply(node);
60+
it = node.it;
61+
return res;
62+
}
63+
64+
private:
65+
Ex_ptr ex;
66+
};
67+
68+
// Trampoline class which allows extending the virtual base class PyAlgorithm
69+
// inside Python
70+
class PyAlgorithmTrampoline : public PyAlgorithm
71+
{
72+
public:
73+
using PyAlgorithm::PyAlgorithm;
74+
75+
bool py_can_apply(ExNode node) override {
76+
PYBIND11_OVERRIDE_PURE_NAME(
77+
bool,
78+
PyAlgorithm,
79+
"can_apply",
80+
py_can_apply,
81+
node
82+
);
83+
}
84+
85+
Algorithm::result_t py_apply(ExNode node) override {
86+
PYBIND11_OVERRIDE_PURE_NAME(
87+
Algorithm::result_t,
88+
PyAlgorithm,
89+
"apply",
90+
py_apply,
91+
node
92+
);
93+
}
94+
};
95+
96+
97+
PYBIND11_MODULE(_algorithm, m)
98+
{
99+
namespace py = pybind11;
100+
101+
py::class_<PyAlgorithm, PyAlgorithmTrampoline>(m, "Algorithm")
102+
.def(py::init<Ex_ptr>())
103+
.def("can_apply", &PyAlgorithm::py_can_apply)
104+
.def("apply", &PyAlgorithm::py_apply)
105+
.def("apply_generic", &PyAlgorithm::py_apply_generic,
106+
py::arg("deep") = true, py::arg("repeat") = false, py::arg("depth") = 0)
107+
.def("apply_pre_order", &PyAlgorithm::py_apply_pre_order, py::arg("repeat") = false);
108+
109+
m.def("apply_algo_base", apply_algo_base<PyAlgorithm>);
110+
}

core/packages/cdb/utils/develop.cnb

Lines changed: 183 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"cell_type": "latex",
88
"cells": [
99
{
10-
"cell_id": 12015957794285357231,
10+
"cell_id": 798500217262993049,
1111
"cell_origin": "client",
1212
"cell_type": "latex_view",
1313
"source": "\\package{cdb.utils.develop}{Helper functions to aid development, debugging and testing}\n\nThis package contains some standardised functionality to aid in development of library code and Python algorithms"
@@ -22,33 +22,188 @@
2222
"cell_type": "input",
2323
"source": "import inspect"
2424
},
25+
{
26+
"cell_id": 13041134632829120942,
27+
"cell_origin": "client",
28+
"cell_type": "latex",
29+
"cells": [
30+
{
31+
"cell_id": 17870137200133729598,
32+
"cell_origin": "client",
33+
"cell_type": "latex_view",
34+
"source": "\\algorithm{class Algorithm(ex: Ex)}{Base class for user defined tree-traversal algorithms}"
35+
}
36+
],
37+
"hidden": true,
38+
"source": "\\algorithm{class Algorithm(ex: Ex)}{Base class for user defined tree-traversal algorithms}"
39+
},
40+
{
41+
"cell_id": 1319220678577630750,
42+
"cell_origin": "client",
43+
"cell_type": "input",
44+
"source": "from cdb.utils._algorithm import Algorithm"
45+
},
46+
{
47+
"cell_id": 13334862458884777602,
48+
"cell_origin": "client",
49+
"cell_type": "input",
50+
"source": "from cdb.utils._algorithm import apply_algo_base as _apply_algo_base\n\ndef _cast_algo(apply, can_apply = lambda node: True):\n\tif len(inspect.getfullargspec(apply).args) != 1:\n\t\traise TypeError(\"Failed to convert function to algorithm: function must only take one 'node' parameter\")\n\tif len(inspect.getfullargspec(can_apply).args) != 1:\n\t\traise TypeError(\"Failed to convert function to algorithm: predicate must only take one 'node' parameter\")\n\treturn type(apply.__name__, (Algorithm, ), { \n\t\t\"can_apply\": lambda self, node: can_apply(node), \n\t\t\"apply\": lambda self, node: apply(node)\n\t})\n\ndef _create_algo(cls, pre_order):\n\tif Algorithm not in cls.__bases__:\n\t\traise TypeError(\"Algorithm must derive from Algorithm type\")\n\tdef wrapper(ex, *args, deep=True, repeat=False, depth=0, **kwargs):\n\t\tx = cls(ex, *args, **kwargs)\n\t\treturn _apply_algo_base(x, ex, deep, repeat, depth, pre_order)\n\treturn wrapper"
51+
},
52+
{
53+
"cell_id": 2421359616022599147,
54+
"cell_origin": "client",
55+
"cell_type": "latex",
56+
"cells": [
57+
{
58+
"cell_id": 16353153281984723823,
59+
"cell_origin": "client",
60+
"cell_type": "latex_view",
61+
"source": "\\algorithm{algo(pre_order: bool = False, pred: function = (node) -> True) -> function}{Decorator for creating tree-traversal algorithms}\n\nThis decorator takes a function or class derived from \\verb|cdb.utils.develop.Algorithm| and creates a tree-traversal algorithm which\niterates through an expression applying the function or \\verb|apply| method of a class to each node in turn. It optionally takes the\narguments \\verb|pre_order| which determines the order of traversal of the tree (\\verb|False| reverts to post-order iteration) and\n\\verb|pred| which, if decorating a function, will be called at each node in the tree and the function only applied if the predicate\nreturns true. If an \\verb|Algorithm| class is instead decorated, the behaviour of \\verb|pred| is implemented by the \\verb|can_apply|\nmethod which must be overloaded"
62+
}
63+
],
64+
"hidden": true,
65+
"source": "\\algorithm{algo(pre_order: bool = False, pred: function = (node) -> True) -> function}{Decorator for creating tree-traversal algorithms}\n\nThis decorator takes a function or class derived from \\verb|cdb.utils.develop.Algorithm| and creates a tree-traversal algorithm which\niterates through an expression applying the function or \\verb|apply| method of a class to each node in turn. It optionally takes the\narguments \\verb|pre_order| which determines the order of traversal of the tree (\\verb|False| reverts to post-order iteration) and\n\\verb|pred| which, if decorating a function, will be called at each node in the tree and the function only applied if the predicate\nreturns true. If an \\verb|Algorithm| class is instead decorated, the behaviour of \\verb|pred| is implemented by the \\verb|can_apply|\nmethod which must be overloaded"
66+
},
67+
{
68+
"cell_id": 8704625228430657301,
69+
"cell_origin": "client",
70+
"cell_type": "input",
71+
"source": "def algo(*args, **kwargs):\n\t# Detect if we are called without arguments and therefore\n\t# get a single class or function instance. In this instance\n\t# default to post order and a predicate always returning true\n\tif len(args) == 1 and len(kwargs) == 0:\n\t\tif inspect.isfunction(args[0]):\n\t\t\tcls = _cast_algo(args[0])\n\t\telif inspect.isclass(args[0]):\n\t\t\tcls = args[0]\n\t\telse:\n\t\t\traise ValueError(\"algo does not accept positional arguments\")\n\t\treturn _create_algo(cls, pre_order=False)\n\n\t# Otherwise we expect an empty *args and collect the keyword arguments\n\tif len(args) != 0:\n\t\traise ValueError(\"algo does not accept positional arguments\")\n\tpre_order = kwargs.get('pre_order', False)\n\tpred = kwargs.get('pred', lambda node: True)\n\n\t# Then we return a decorator with no arguments which will get applied\n\tdef unwrapped(arg):\n\t\tif inspect.isfunction(arg):\n\t\t\tcls = _cast_algo(arg, pred)\n\t\telse:\n\t\t\tif 'pred' in kwargs:\n\t\t\t\traise ValueError(\"pred supplied to algo decorator, but this does not apply to class being decorated\")\n\t\t\tcls = arg\n\t\treturn _create_algo(cls, pre_order=pre_order)\n\treturn unwrapped"
72+
},
73+
{
74+
"cell_id": 2405738556624578142,
75+
"cell_origin": "client",
76+
"cell_type": "input",
77+
"cells": [
78+
{
79+
"cell_id": 4043273741898506206,
80+
"cell_origin": "server",
81+
"cell_type": "latex_view",
82+
"cells": [
83+
{
84+
"cell_id": 11767503884852531506,
85+
"cell_origin": "server",
86+
"cell_type": "input_form",
87+
"source": "A^{\\mu} + B^{\\mu}"
88+
}
89+
],
90+
"source": "\\begin{dmath*}{}A^{\\mu}+B^{\\mu}\\end{dmath*}"
91+
}
92+
],
93+
"ignore_on_import": true,
94+
"source": "@algo\ndef switch_indices(node):\n\tif node.parent_rel == parent_rel_t.sub:\n\t\tnode.parent_rel = parent_rel_t.super\n\t\treturn result_t.changed\n\tif node.parent_rel == parent_rel_t.super:\n\t\tnode.parent_rel = parent_rel_t.sub\n\t\treturn result_t.changed\n\treturn result_t.unchanged\n\n# also takes optional 'deep', 'repeat' and 'depth' arguments\nswitch_indices($A_{\\mu} + B_{\\mu}$);"
95+
},
96+
{
97+
"cell_id": 9340011096170931285,
98+
"cell_origin": "client",
99+
"cell_type": "input",
100+
"cells": [
101+
{
102+
"cell_id": 14835945528453774882,
103+
"cell_origin": "server",
104+
"cell_type": "latex_view",
105+
"cells": [
106+
{
107+
"cell_id": 15750930497414250141,
108+
"cell_origin": "server",
109+
"cell_type": "input_form",
110+
"source": "2y"
111+
}
112+
],
113+
"source": "\\begin{dmath*}{}2y\\end{dmath*}"
114+
}
115+
],
116+
"ignore_on_import": true,
117+
"source": "@algo(pre_order=True, pred=lambda node: node.name == 'x')\ndef x_to_y(node):\n\tnode.name = 'y'\n\treturn result_t.changed\n\nx_to_y($x + y$);"
118+
},
119+
{
120+
"cell_id": 15634397355979861807,
121+
"cell_origin": "client",
122+
"cell_type": "input",
123+
"cells": [
124+
{
125+
"cell_id": 960346398456001684,
126+
"cell_origin": "server",
127+
"cell_type": "latex_view",
128+
"cells": [
129+
{
130+
"cell_id": 1313190532695810937,
131+
"cell_origin": "server",
132+
"cell_type": "input_form",
133+
"source": "2t_{\\mu}"
134+
}
135+
],
136+
"source": "\\begin{dmath*}{}2t_{\\mu}\\end{dmath*}"
137+
}
138+
],
139+
"ignore_on_import": true,
140+
"source": "@algo\nclass a_to_b(Algorithm):\n\tdef __init__(self, ex, a, b):\n\t\tAlgorithm.__init__(self, ex)\n\t\tself.a, self.b = a, b\n\t\n\tdef can_apply(self, node):\n\t\treturn node.name == self.a.head()\n\n\tdef apply(self, node):\n\t\tnode.name = self.b.head()\n\t\treturn result_t.changed\n\na_to_b($s_{\\mu} + t_{\\mu}$, $s_{\\mu}$, $t_{\\mu}$);"
141+
},
142+
{
143+
"cell_id": 10135200674124616797,
144+
"cell_origin": "client",
145+
"cell_type": "input",
146+
"ignore_on_import": true,
147+
"source": "try:\n\t@algo\n\tdef function_with_too_many_arguments(a,b,c):\n\t\tpass\n\traise AssertionError(\"TypeError not raised\")\nexcept TypeError:\n\tpass\n\ntry:\n\t@algo\n\tclass does_not_inherit_from_Algorithm:\n\t\tpass\n\traise AssertionError(\"TypeError not raised\")\nexcept TypeError:\n\tpass\n\ntry:\n\t@algo(pred=lambda n: n.name == 'x')\n\tclass does_not_need_pred(Algorithm):\n\t\tdef can_apply(self,node): return n.name =='x'\n\t\tdef apply(self,node): return result_t.unchanged\n\traise AssertionError(\"ValueError not raised\")\nexcept ValueError:\n\tpass"
148+
},
25149
{
26150
"cell_id": 15845148038835594810,
27151
"cell_origin": "client",
28152
"cell_type": "latex",
29153
"cells": [
30154
{
31-
"cell_id": 11938570826720444108,
155+
"cell_id": 8739236297038682978,
32156
"cell_origin": "client",
33157
"cell_type": "latex_view",
34-
"source": "\\algorithm{time_algo(algo: function, ex: Ex, *args: <mixed>, iterations: int = 100) -> float}{Simple function to time the \nexecution of an algorithm with given inputs.}\n\nThe arguments in *args are passed directly, but ex is copied before each\ninvocation and so remains unmodified."
158+
"source": "\\algorithm{time_algo(algo: function, ex: Ex, *args: <mixed>, iterations: int = 100) -> float}{Simple function to time the execution of an algorithm with given inputs.}\n\nThe arguments in *args are passed directly, but ex is copied before each\ninvocation and so remains unmodified."
35159
}
36160
],
161+
"hidden": true,
37162
"source": "\\algorithm{time_algo(algo: function, ex: Ex, *args: <mixed>, iterations: int = 100) -> float}{Simple function to time the execution of an algorithm with given inputs.}\n\nThe arguments in *args are passed directly, but ex is copied before each\ninvocation and so remains unmodified."
38163
},
39164
{
40165
"cell_id": 5937432672654218849,
41166
"cell_origin": "client",
42167
"cell_type": "input",
43-
"source": "def time_algo(algo, ex, *args, iterations=100):\n\ts = Stopwatch()\n\tfor i in range(iterations):\n\t\ttmp := @(ex);\n\t\ts.start()\n\t\talgo(tmp, *args)\n\t\ts.stop()\n\treturn (s.seconds() + s.useconds() / 1000000) / iterations"
168+
"source": "from datetime import timedelta\n\ndef time_algo(algo, ex, *args, iterations=100):\n\ts = Stopwatch()\n\tfor i in range(iterations):\n\t\ttmp := @(ex);\n\t\ts.start()\n\t\talgo(tmp, *args)\n\t\ts.stop()\n\treturn timedelta(seconds=s.seconds()/iterations, microseconds=s.useconds()/iterations)"
169+
},
170+
{
171+
"cell_id": 17198486848786764579,
172+
"cell_origin": "client",
173+
"cell_type": "input",
174+
"cells": [
175+
{
176+
"cell_id": 16504503887878721184,
177+
"cell_origin": "server",
178+
"cell_type": "verbatim",
179+
"source": "\\begin{verbatim}0:00:00.008331\\end{verbatim}"
180+
}
181+
],
182+
"ignore_on_import": true,
183+
"source": "time_algo(substitute, $a + b$, $a -> b$);"
184+
},
185+
{
186+
"cell_id": 12791856048123301106,
187+
"cell_origin": "client",
188+
"cell_type": "input",
189+
"cells": [
190+
{
191+
"cell_id": 17622425082194156367,
192+
"cell_origin": "server",
193+
"cell_type": "verbatim",
194+
"source": "\\begin{verbatim}0:00:00.003194\\end{verbatim}"
195+
}
196+
],
197+
"ignore_on_import": true,
198+
"source": "def times_2(ex):\n\treturn ex * $2$\n\ntime_algo(times_2, $a$);"
44199
},
45200
{
46201
"cell_id": 15332315745938100730,
47202
"cell_origin": "client",
48203
"cell_type": "latex",
49204
"cells": [
50205
{
51-
"cell_id": 11894560304174452086,
206+
"cell_id": 8935506242999741244,
52207
"cell_origin": "client",
53208
"cell_type": "latex_view",
54209
"source": "\\algorithm{CadabraTestError}{Exception derived from AssertionError raised by testing functions when an assertion fails}"
@@ -69,35 +224,50 @@
69224
"cell_type": "latex",
70225
"cells": [
71226
{
72-
"cell_id": 14998088897783344386,
227+
"cell_id": 11588087231457837709,
73228
"cell_origin": "client",
74229
"cell_type": "latex_view",
75-
"source": "\\algorithm{test_algo(expected: Ex, verbose: bool)}{Decorator to aid defining unit tests for algorithms.}\n\nThis\tadds the boilerplate code and adds an assert for the test."
230+
"source": "\\algorithm{test_algo(expected: Ex, throw_on_fail: bool)}{Decorator to aid defining unit tests for algorithms.}\n\nThis\tadds the boilerplate code and adds an assert for the test."
76231
}
77232
],
78233
"hidden": true,
79-
"source": "\\algorithm{test_algo(expected: Ex, verbose: bool)}{Decorator to aid defining unit tests for algorithms.}\n\nThis\tadds the boilerplate code and adds an assert for the test."
234+
"source": "\\algorithm{test_algo(expected: Ex, throw_on_fail: bool)}{Decorator to aid defining unit tests for algorithms.}\n\nThis\tadds the boilerplate code and adds an assert for the test."
80235
},
81236
{
82237
"cell_id": 15123798302514262900,
83238
"cell_origin": "client",
84239
"cell_type": "input",
85-
"source": "def test_algo(expected, verbose=False):\n\t\"\"\"\n\tExample usage:\n\t\t@test_algo($a + b + c$)\n\t\tdef sort_sum_test():\n\t\t\tex := b + a + c.\n\t\t\treturn sort_sum(ex)\n\t\"\"\"\n\tdef decorator(func):\n\t\tdef wrapper(*args, **kwargs):\n\t\t\tres = func(*args, **kwargs)\n\t\t\tif res == expected:\n\t\t\t\tprint(func.__name__ + \" passed\")\n\t\t\telse:\n\t\t\t\tprint(func.__name__ + \" FAILED\")\n\t\t\t\tprint(\"Expected: \" + str(expected))\n\t\t\t\tif verbose: print(tree(expected))\n\t\t\t\tprint(\"Produced: \" + str(res))\n\t\t\t\tif verbose: print(tree(res))\n\t\t\t\traise CadabraTestError\n\t\t\treturn res\n\t\treturn wrapper\n\treturn decorator"
240+
"source": "def test_algo(expected, throw_on_fail=False):\n\tdef decorator(func):\n\t\tdef wrapper(*args, **kwargs):\n\t\t\tres = func(*args, **kwargs)\n\t\t\tif res == expected:\n\t\t\t\tprint(func.__name__ + \" passed\")\n\t\t\telse:\n\t\t\t\tprint(func.__name__ + \" FAILED\")\n\t\t\t\tprint(\" Expected: \" + str(expected))\n\t\t\t\tprint(\" Produced: \" + str(res))\n\t\t\t\tif throw_on_fail: raise CadabraTestError\n\t\t\treturn res\n\t\treturn wrapper\n\treturn decorator"
241+
},
242+
{
243+
"cell_id": 2554539507442301090,
244+
"cell_origin": "client",
245+
"cell_type": "input",
246+
"cells": [
247+
{
248+
"cell_id": 17109594158398788775,
249+
"cell_origin": "server",
250+
"cell_type": "output",
251+
"source": "\\begin{verbatim}sort_sum_test01 passed\nsort_sum_test02 FAILED\n Expected: a + b\n Produced: a + c\nsort_sum_test03 FAILED\n Expected: a + b\n Produced: a + c\n\\end{verbatim}"
252+
}
253+
],
254+
"ignore_on_import": true,
255+
"source": "@test_algo($a + b + c$)\ndef sort_sum_test01():\n\tex := b + a + c.\n\treturn sort_sum(ex)\nsort_sum_test01()\n\n@test_algo($a + b$)\ndef sort_sum_test02():\n\tex := c + a.\n\treturn sort_sum(ex)\nsort_sum_test02()\n\n@test_algo($a + b$, throw_on_fail=True)\ndef sort_sum_test03():\n\tex := c + a.\n\treturn sort_sum(ex)\n\ntry:\n\tsort_sum_test03()\n\traise AssertionError(\"CadabraTestError not raised!\")\nexcept CadabraTestError:\n\tpass"
86256
},
87257
{
88258
"cell_id": 17571440201905887785,
89259
"cell_origin": "client",
90260
"cell_type": "latex",
91261
"cells": [
92262
{
93-
"cell_id": 309410713532683075,
263+
"cell_id": 12853042454041156620,
94264
"cell_origin": "client",
95265
"cell_type": "latex_view",
96-
"source": "\\algorithm{inherit_kernel() -> Kernel}{Find a kernel in the stack.}\n\nMove up stack frames until one which defines the \\_\\_cdbkernel\\_\\_ variable is located and return \nit. If no Kernel object is foundthen None is returned"
266+
"source": "\\algorithm{inherit_kernel() -> Kernel}{Find a kernel in the stack.}\n\nMove up stack frames until one which defines the \\verb|__cdbkernel__| variable is located and return \nit. If no Kernel object is found then None is returned"
97267
}
98268
],
99269
"hidden": true,
100-
"source": "\\algorithm{inherit_kernel() -> Kernel}{Find a kernel in the stack.}\n\nMove up stack frames until one which defines the \\_\\_cdbkernel\\_\\_ variable is located and return \nit. If no Kernel object is foundthen None is returned"
270+
"source": "\\algorithm{inherit_kernel() -> Kernel}{Find a kernel in the stack.}\n\nMove up stack frames until one which defines the \\verb|__cdbkernel__| variable is located and return \nit. If no Kernel object is found then None is returned"
101271
},
102272
{
103273
"cell_id": 460414117077303111,
@@ -109,6 +279,7 @@
109279
"cell_id": 13850153861758424314,
110280
"cell_origin": "client",
111281
"cell_type": "input",
282+
"ignore_on_import": true,
112283
"source": ""
113284
}
114285
],

core/pythoncdb/py_ex.cc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -598,6 +598,7 @@ namespace cadabra {
598598
.def("matches", &Ex_matches_Ex)
599599
.def("state", &Ex::state)
600600
.def("reset", &Ex::reset_state)
601+
.def("copy", [](const Ex& ex) { return std::make_shared<Ex>(ex); })
601602
.def("changed", &Ex::changed_state)
602603
.def("cleanup", &Ex_cleanup)
603604
.def("__add__", static_cast<Ex_ptr(*)(const Ex_ptr, const ExNode)>(&Ex_add), py::is_operator{})

0 commit comments

Comments
 (0)