PolyFEM
Loading...
Searching...
No Matches
ExpressionValue.cpp
Go to the documentation of this file.
1#include "ExpressionValue.hpp"
2
5
6#include <units/units.hpp>
7
8#include <igl/PI.h>
9
10#include <tinyexpr.h>
11#include <filesystem>
12#include <memory>
13#ifdef POLYFEM_WITH_PYTHON
14// pybind11 enables a debug-only reference counter when NDEBUG is not defined.
15// On MSVC that path can fail with C2480 because it uses a function-local
16// thread_local variable. Keep the workaround scoped to pybind11's headers.
17#if defined(_MSC_VER) && !defined(NDEBUG) && !defined(PYBIND11_HANDLE_REF_DEBUG)
18#define POLYFEM_RESTORE_NDEBUG_AFTER_PYBIND11
19#define NDEBUG
20#endif
21#include <pybind11/embed.h>
22#include <pybind11/pybind11.h>
23#include <pybind11/stl.h>
24#ifdef POLYFEM_RESTORE_NDEBUG_AFTER_PYBIND11
25#undef NDEBUG
26#undef POLYFEM_RESTORE_NDEBUG_AFTER_PYBIND11
27#endif
28#endif
29
30#include <iostream>
31
32namespace polyfem
33{
34 using namespace io;
35
36 namespace utils
37 {
38#ifdef POLYFEM_WITH_PYTHON
39 namespace py = pybind11;
40
41 namespace
42 {
43 class PythonInterpreter
44 {
45 public:
46 static PythonInterpreter &instance()
47 {
48 static PythonInterpreter interpreter;
49 return interpreter;
50 }
51
52 PythonInterpreter(const PythonInterpreter &) = delete;
53 PythonInterpreter &operator=(const PythonInterpreter &) = delete;
54
55 private:
56 PythonInterpreter()
57 {
58 if (!Py_IsInitialized())
59 {
60 // Keep the embedded interpreter alive for the process lifetime.
61 // Python-backed ExpressionValues may be destroyed during static
62 // teardown, and finalizing first makes their py::object cleanup unsafe.
63#ifdef POLYFEM_PYTHON_EXECUTABLE
64 PyConfig config;
65 PyConfig_InitPythonConfig(&config);
66
67 wchar_t *program_raw = Py_DecodeLocale(POLYFEM_PYTHON_EXECUTABLE, nullptr);
68 if (program_raw == nullptr)
69 {
70 PyConfig_Clear(&config);
71 log_and_throw_error("Failed to decode Python executable path.");
72 }
73
74 PyStatus status = PyConfig_SetString(&config, &config.program_name, program_raw);
75 PyMem_RawFree(program_raw);
76 if (PyStatus_Exception(status))
77 {
78 PyConfig_Clear(&config);
79 log_and_throw_error("Failed to configure embedded Python program_name.");
80 }
81
82 guard_ = new py::scoped_interpreter(&config);
83#else
84 guard_ = new py::scoped_interpreter();
85#endif
86 }
87 }
88
89 ~PythonInterpreter() = default;
90
91 py::scoped_interpreter *guard_ = nullptr;
92 };
93
94 void ensure_python_interpreter()
95 {
96 try
97 {
98 (void)PythonInterpreter::instance();
99 }
100 catch (const std::exception &e)
101 {
102 log_and_throw_error(fmt::format("Failed to initialize embedded Python: {}", e.what()));
103 }
104 }
105
106 std::shared_ptr<py::object> make_python_object_holder(py::object value)
107 {
108 return std::shared_ptr<py::object>(
109 new py::object(std::move(value)),
110 [](py::object *object) {
111 if (Py_IsInitialized())
112 {
113 py::gil_scoped_acquire gil;
114 delete object;
115 }
116 else
117 {
118 (void)object->release();
119 delete object;
120 }
121 });
122 }
123
124 py::object load_python_value_function(const std::string &path, const std::string &function_name)
125 {
126 ensure_python_interpreter();
127 py::gil_scoped_acquire gil;
128
129 py::module_ importlib_util = py::module_::import("importlib.util");
130 py::module_ pathlib = py::module_::import("pathlib");
131
132 py::object resolved_path = pathlib.attr("Path")(path).attr("resolve")();
133 std::string module_name = resolved_path.attr("stem").cast<std::string>();
134 std::string resolved_path_str = py::str(resolved_path).cast<std::string>();
135
136 py::object spec = importlib_util.attr("spec_from_file_location")(module_name, resolved_path_str);
137 if (spec.is_none())
138 log_and_throw_error(fmt::format("Unable to create Python module spec from '{}'", resolved_path_str));
139
140 py::object loader = spec.attr("loader");
141 if (loader.is_none())
142 log_and_throw_error(fmt::format("Python module '{}' has no loader", resolved_path_str));
143
144 py::object module = importlib_util.attr("module_from_spec")(spec);
145 loader.attr("exec_module")(module);
146
147 if (!py::hasattr(module, function_name.c_str()))
148 log_and_throw_error(fmt::format("Python expression file '{}' must define a function named '{}'", resolved_path_str, function_name));
149
150 py::object value = module.attr(function_name.c_str());
151 if (!PyCallable_Check(value.ptr()))
152 log_and_throw_error(fmt::format("Python attribute '{}' in '{}' is not callable", function_name, resolved_path_str));
153
154 return value;
155 }
156 } // namespace
157
158 void ExpressionValue::init_python(const std::string &path, const std::string &function_name)
159 {
160 clear();
161
162 py::object value = load_python_value_function(path, function_name);
163 auto callable = make_python_object_holder(std::move(value));
164
165 sfunc_ = [callable](double x, double y, double z, double t, int index) -> double {
166 py::gil_scoped_acquire gil;
167 py::object out = (*callable)(x, y, z, t, index);
168
169 try
170 {
171 return out.cast<double>();
172 }
173 catch (const py::cast_error &)
174 {
175 log_and_throw_error("Python expression must return a scalar convertible to double");
176 return 0;
177 }
178 };
179 }
180
181#endif
182
183 static double min(double a, double b) { return a < b ? a : b; }
184 static double max(double a, double b) { return a > b ? a : b; }
185 static double smoothstep(double a)
186 {
187 if (a < 0)
188 return 0;
189 else if (a > 1)
190 return 1;
191 else
192 return (3 * pow(a, 2)) - (2 * pow(a, 3));
193 }
194 static double half_smoothstep(double a)
195 {
196 double b = (a + 1.) / 2.;
197 return 2. * smoothstep(b) - 1.;
198 }
199 static double deg2rad(double d) { return d * igl::PI / 180.0; }
200 static double rotate_2D_x(double x, double y, double theta)
201 {
202 return x * cos(theta) - y * sin(theta);
203 }
204 static double rotate_2D_y(double x, double y, double theta)
205 {
206 return x * sin(theta) + y * cos(theta);
207 }
208 static double smooth_abs(double x, double k)
209 {
210 return tanh(k * x) * x;
211 }
212 static double iflargerthanzerothenelse(double check, double ttrue, double ffalse)
213 {
214 return check >= 0 ? ttrue : ffalse;
215 }
216 static double sign(double x)
217 {
218 return (0 < x) - (x < 0);
219 }
220 static double compare(double a, double b)
221 {
222 return a < b ? 1.0 : 0.0;
223 }
224
229
231 {
232 expr_ = "";
233 mat_.resize(0, 0);
234 mat_expr_ = {};
235 sfunc_ = nullptr;
236 tfunc_ = nullptr;
237 value_ = 0;
238 index_ = -1;
239 }
240
241 void ExpressionValue::init(const double val)
242 {
243 clear();
244
245 value_ = val;
246 }
247
248 void ExpressionValue::init(const Eigen::MatrixXd &val)
249 {
250 clear();
251
252 mat_ = val;
253 }
254
255 void ExpressionValue::init(const std::string &expr, const io::ResourceIO &resources)
256 {
257 clear();
258
259 if (expr.empty())
260 {
261 return;
262 }
263
264 try
265 {
266 if (resources.exists(expr) && !resources.is_group(expr))
267 {
268 mat_ = resources.read_matrix(expr);
269 return;
270 }
271 }
272 catch (const std::exception &)
273 {
274 }
275
276 expr_ = expr;
277
278 double x = 0, y = 0, z = 0, t = 0;
279
280 std::vector<te_variable> vars = {
281 {"x", &x, TE_VARIABLE},
282 {"y", &y, TE_VARIABLE},
283 {"z", &z, TE_VARIABLE},
284 {"t", &t, TE_VARIABLE},
285 {"min", (const void *)min, TE_FUNCTION2},
286 {"max", (const void *)max, TE_FUNCTION2},
287 {"smoothstep", (const void *)smoothstep, TE_FUNCTION1},
288 {"half_smoothstep", (const void *)half_smoothstep, TE_FUNCTION1},
289 {"deg2rad", (const void *)deg2rad, TE_FUNCTION1},
290 {"rotate_2D_x", (const void *)rotate_2D_x, TE_FUNCTION3},
291 {"rotate_2D_y", (const void *)rotate_2D_y, TE_FUNCTION3},
292 {"if", (const void *)iflargerthanzerothenelse, TE_FUNCTION3},
293 {"compare", (const void *)compare, TE_FUNCTION2},
294 {"smooth_abs", (const void *)smooth_abs, TE_FUNCTION2},
295 {"sign", (const void *)sign, TE_FUNCTION1},
296 };
297
298 int err;
299 te_expr *tmp = te_compile(expr.c_str(), vars.data(), vars.size(), &err);
300 if (!tmp)
301 {
302 logger().error("Unable to parse: {}", expr);
303 logger().error("Error near character {}.", err);
304 log_and_throw_error("Invalid expression '{}'.", expr);
305 }
306 te_free(tmp);
307 }
308
309 void ExpressionValue::init(const json &vals, const io::ResourceIO &resources)
310 {
311 clear();
312
313 if (vals.is_number())
314 {
315 init(vals.get<double>());
316 }
317 else if (vals.is_boolean())
318 {
319 init(vals.get<bool>() ? 1. : 0.);
320 }
321 else if (vals.is_array())
322 {
323 if (vals.empty() || vals[0].is_number())
324 {
325 mat_.resize(vals.size(), 1);
326
327 for (int i = 0; i < mat_.size(); ++i)
328 {
329 if (!vals[i].is_number())
330 log_and_throw_error("Expression arrays must contain either only numbers or only expressions.");
331 mat_(i) = vals[i].get<double>();
332 }
333 }
334 else
335 {
336 mat_expr_ = std::vector<ExpressionValue>(vals.size());
337
338 for (int i = 0; i < vals.size(); ++i)
339 {
340 mat_expr_[i].init(vals[i], resources);
341 if (unit_type_set_)
342 {
343 mat_expr_[i].unit_type_ = unit_type_;
344 mat_expr_[i].unit_type_set_ = true;
345 }
346 }
347 }
348
349 if (t_index_.size() > 0)
350 if (mat_.size() != t_index_.size() && mat_expr_.size() != t_index_.size())
351 logger().error("Specifying varying dirichlet over time, however 'time_reference' does not match dirichlet boundary conditions.");
352 }
353 else if (vals.is_object())
354 {
355 units::precise_unit unit;
356 if (vals.contains("unit"))
357 {
358 if (!vals["unit"].is_string())
359 log_and_throw_error("Expression object 'unit' must be a string.");
360
361 const std::string unit_str = vals["unit"].get<std::string>();
362 if (!unit_str.empty())
363 unit = units::unit_from_string(unit_str);
364 }
365
366 if (vals.contains("file_name"))
367 {
368#ifndef POLYFEM_WITH_PYTHON
370 "Python expression '{}' requested, but PolyFEM was built without Python support. "
371 "Reconfigure with -DPOLYFEM_WITH_PYTHON=ON to enable Python expressions.",
372 vals["file_name"].dump());
373#else
374 if (!vals["file_name"].is_string())
375 log_and_throw_error("Python expression 'file_name' must be a string.");
376 if (!vals.contains("function_name") || !vals["function_name"].is_string())
377 log_and_throw_error("Python expression '{}' must include a string 'function_name'.", vals["file_name"].get<std::string>());
378
379 const std::string path = resources.materialize(vals["file_name"].get<std::string>()).string();
380 const std::string function_name = vals["function_name"].get<std::string>();
381
382 init_python(path, function_name);
383 unit_ = unit;
384 return;
385#endif
386 }
387
388 if (!vals.contains("value"))
389 log_and_throw_error("Expression object must contain either 'value' or 'file_name'.");
390
391 init(vals["value"], resources);
392 unit_ = unit;
393 }
394 else
395 {
396 init(vals.get<std::string>(), resources);
397 }
398 }
399
400 void ExpressionValue::init(const std::function<double(double x, double y, double z)> &func)
401 {
402 clear();
403
404 sfunc_ = [func](double x, double y, double z, double t, int index) { return func(x, y, z); };
405 }
406
407 void ExpressionValue::init(const std::function<double(double x, double y, double z, double t)> &func)
408 {
409 clear();
410 sfunc_ = [func](double x, double y, double z, double t, int index) { return func(x, y, z, t); };
411 }
412
413 void ExpressionValue::init(const std::function<double(double x, double y, double z, double t, int index)> &func)
414 {
415 clear();
416 sfunc_ = func;
417 }
418
419 void ExpressionValue::init(const std::function<Eigen::MatrixXd(double x, double y, double z)> &func, const int coo)
420 {
421 clear();
422
423 tfunc_ = [func](double x, double y, double z, double t) { return func(x, y, z); };
424 tfunc_coo_ = coo;
425 }
426
427 void ExpressionValue::init(const std::function<Eigen::MatrixXd(double x, double y, double z, double t)> &func, const int coo)
428 {
429 clear();
430
431 tfunc_ = func;
432 tfunc_coo_ = coo;
433 }
434
436 {
437 if (t.is_array())
438 {
439 for (int i = 0; i < t.size(); ++i)
440 {
441 t_index_[std::round(t[i].get<double>() * 1000.) / 1000.] = i;
442 }
443
444 if (mat_.size() != t_index_.size() && mat_expr_.size() != t_index_.size())
445 logger().error("Specifying varying dirichlet over time, however 'time_reference' does not match dirichlet boundary conditions.");
446 }
447 }
448
449 double ExpressionValue::operator()(double x, double y, double z, double t, int index) const
450 {
451 double result;
452 if (expr_.empty())
453 {
454 if (t_index_.size() > 0)
455 {
456 t = std::round(t * 1000.) / 1000.;
457 if (t_index_.count(t) != 0)
458 {
459 if (mat_.size() > 0)
460 return mat_(t_index_.at(t));
461 else if (mat_expr_.size() > 0)
462 return mat_expr_[t_index_.at(t)](x, y, z, t, index);
463 }
464 else
465 {
466 logger().error("Cannot find dirichlet condition for time step {}.", t);
467 return 0;
468 }
469 }
470
471 if (mat_.size() > 0)
472 result = mat_(index_ >= 0 ? index_ : index);
473 else if (sfunc_)
474 result = sfunc_(x, y, z, t, index);
475 else if (tfunc_)
476 result = tfunc_(x, y, z, t)(tfunc_coo_);
477 else
478 result = value_;
479 }
480 else
481 {
482
483 std::vector<te_variable> vars = {
484 {"x", &x, TE_VARIABLE},
485 {"y", &y, TE_VARIABLE},
486 {"z", &z, TE_VARIABLE},
487 {"t", &t, TE_VARIABLE},
488 {"min", (const void *)min, TE_FUNCTION2},
489 {"max", (const void *)max, TE_FUNCTION2},
490 {"smoothstep", (const void *)smoothstep, TE_FUNCTION1},
491 {"half_smoothstep", (const void *)half_smoothstep, TE_FUNCTION1},
492 {"deg2rad", (const void *)deg2rad, TE_FUNCTION1},
493 {"rotate_2D_x", (const void *)rotate_2D_x, TE_FUNCTION3},
494 {"rotate_2D_y", (const void *)rotate_2D_y, TE_FUNCTION3},
495 {"if", (const void *)iflargerthanzerothenelse, TE_FUNCTION3},
496 {"compare", (const void *)compare, TE_FUNCTION2},
497 {"smooth_abs", (const void *)smooth_abs, TE_FUNCTION2},
498 {"sign", (const void *)sign, TE_FUNCTION1},
499 };
500
501 int err;
502 te_expr *tmp = te_compile(expr_.c_str(), vars.data(), vars.size(), &err);
503 if (!tmp)
504 {
505 logger().error("Unable to parse: {}", expr_);
506 logger().error("Error near character {}.", err);
507 log_and_throw_error("Invalid expression '{}'.", expr_);
508 }
509 result = te_eval(tmp);
510 te_free(tmp);
511 }
512
513 if (!unit_.base_units().empty())
514 {
515 if (!unit_.is_convertible(unit_type_))
516 log_and_throw_error(fmt::format("Cannot convert {} to {}", units::to_string(unit_), units::to_string(unit_type_)));
517
518 result = units::convert(result, unit_, unit_type_);
519 }
520
521 return result;
522 }
523 } // namespace utils
524} // namespace polyfem
double val
Definition Assembler.cpp:90
ElementAssemblyValues vals
Definition Assembler.cpp:26
int y
int z
int x
Read-only interface to a hierarchical collection of simulation resources.
virtual std::filesystem::path materialize(const std::string &path) const =0
virtual bool exists(const std::string &path) const =0
virtual Eigen::MatrixXd read_matrix(const std::string &path) const =0
virtual bool is_group(const std::string &path) const =0
std::vector< ExpressionValue > mat_expr_
std::function< double(double x, double y, double z, double t, int index)> sfunc_
std::function< Eigen::MatrixXd(double x, double y, double z, double t)> tfunc_
std::map< double, int > t_index_
double operator()(double x, double y, double z=0, double t=0, int index=-1) const
void init(const json &vals, const io::ResourceIO &resources)
spdlog::logger & logger()
Retrieves the current logger.
Definition Logger.cpp:44
nlohmann::json json
Definition Common.hpp:9
void log_and_throw_error(const std::string &msg)
Definition Logger.cpp:73