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