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 }
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 std::string &root_path)
256 {
257 clear();
258
259 if (expr.empty())
260 {
261 return;
262 }
263
264 const auto path = std::filesystem::path(utils::resolve_path(expr, root_path));
265
266 try
267 {
268 if (std::filesystem::is_regular_file(path))
269 {
270 read_matrix(path.string(), mat_);
271 return;
272 }
273 }
274 catch (const std::filesystem::filesystem_error &e)
275 {
276 }
277
278 expr_ = expr;
279
280 double x = 0, y = 0, z = 0, t = 0;
281
282 std::vector<te_variable> vars = {
283 {"x", &x, TE_VARIABLE},
284 {"y", &y, TE_VARIABLE},
285 {"z", &z, TE_VARIABLE},
286 {"t", &t, TE_VARIABLE},
287 {"min", (const void *)min, TE_FUNCTION2},
288 {"max", (const void *)max, TE_FUNCTION2},
289 {"smoothstep", (const void *)smoothstep, TE_FUNCTION1},
290 {"half_smoothstep", (const void *)half_smoothstep, TE_FUNCTION1},
291 {"deg2rad", (const void *)deg2rad, TE_FUNCTION1},
292 {"rotate_2D_x", (const void *)rotate_2D_x, TE_FUNCTION3},
293 {"rotate_2D_y", (const void *)rotate_2D_y, TE_FUNCTION3},
294 {"if", (const void *)iflargerthanzerothenelse, TE_FUNCTION3},
295 {"compare", (const void *)compare, TE_FUNCTION2},
296 {"smooth_abs", (const void *)smooth_abs, TE_FUNCTION2},
297 {"sign", (const void *)sign, TE_FUNCTION1},
298 };
299
300 int err;
301 te_expr *tmp = te_compile(expr.c_str(), vars.data(), vars.size(), &err);
302 if (!tmp)
303 {
304 logger().error("Unable to parse: {}", expr);
305 logger().error("Error near character {}.", err);
306 log_and_throw_error("Invalid expression '{}'.", expr);
307 }
308 te_free(tmp);
309 }
310
311 void ExpressionValue::init(const json &vals, const std::string &root_path)
312 {
313 clear();
314
315 if (vals.is_number())
316 {
317 init(vals.get<double>());
318 }
319 else if (vals.is_array())
320 {
321 if (vals.empty() || vals[0].is_number())
322 {
323 mat_.resize(vals.size(), 1);
324
325 for (int i = 0; i < mat_.size(); ++i)
326 {
327 if (!vals[i].is_number())
328 log_and_throw_error("Expression arrays must contain either only numbers or only expressions.");
329 mat_(i) = vals[i].get<double>();
330 }
331 }
332 else
333 {
334 mat_expr_ = std::vector<ExpressionValue>(vals.size());
335
336 for (int i = 0; i < vals.size(); ++i)
337 {
338 mat_expr_[i].init(vals[i], root_path);
339 if (unit_type_set_)
340 {
341 mat_expr_[i].unit_type_ = unit_type_;
342 mat_expr_[i].unit_type_set_ = true;
343 }
344 }
345 }
346
347 if (t_index_.size() > 0)
348 if (mat_.size() != t_index_.size() && mat_expr_.size() != t_index_.size())
349 logger().error("Specifying varying dirichlet over time, however 'time_reference' does not match dirichlet boundary conditions.");
350 }
351 else if (vals.is_object())
352 {
353 units::precise_unit unit;
354 if (vals.contains("unit"))
355 {
356 if (!vals["unit"].is_string())
357 log_and_throw_error("Expression object 'unit' must be a string.");
358
359 const std::string unit_str = vals["unit"].get<std::string>();
360 if (!unit_str.empty())
361 unit = units::unit_from_string(unit_str);
362 }
363
364 if (vals.contains("file_name"))
365 {
366#ifndef POLYFEM_WITH_PYTHON
368 "Python expression '{}' requested, but PolyFEM was built without Python support. "
369 "Reconfigure with -DPOLYFEM_WITH_PYTHON=ON to enable Python expressions.",
370 vals["file_name"].dump());
371#else
372 if (!vals["file_name"].is_string())
373 log_and_throw_error("Python expression 'file_name' must be a string.");
374 if (!vals.contains("function_name") || !vals["function_name"].is_string())
375 log_and_throw_error("Python expression '{}' must include a string 'function_name'.", vals["file_name"].get<std::string>());
376
377 const std::string path = utils::resolve_path(vals["file_name"].get<std::string>(), root_path);
378 const std::string function_name = vals["function_name"].get<std::string>();
379
380 init_python(path, function_name);
381 unit_ = unit;
382 return;
383#endif
384 }
385
386 if (!vals.contains("value"))
387 log_and_throw_error("Expression object must contain either 'value' or 'file_name'.");
388
389 init(vals["value"], root_path);
390 unit_ = unit;
391 }
392 else
393 {
394 init(vals.get<std::string>(), root_path);
395 }
396 }
397
398 void ExpressionValue::init(const std::function<double(double x, double y, double z)> &func)
399 {
400 clear();
401
402 sfunc_ = [func](double x, double y, double z, double t, int index) { return func(x, y, z); };
403 }
404
405 void ExpressionValue::init(const std::function<double(double x, double y, double z, double t)> &func)
406 {
407 clear();
408 sfunc_ = [func](double x, double y, double z, double t, int index) { return func(x, y, z, t); };
409 }
410
411 void ExpressionValue::init(const std::function<double(double x, double y, double z, double t, int index)> &func)
412 {
413 clear();
414 sfunc_ = func;
415 }
416
417 void ExpressionValue::init(const std::function<Eigen::MatrixXd(double x, double y, double z)> &func, const int coo)
418 {
419 clear();
420
421 tfunc_ = [func](double x, double y, double z, double t) { return func(x, y, z); };
422 tfunc_coo_ = coo;
423 }
424
425 void ExpressionValue::init(const std::function<Eigen::MatrixXd(double x, double y, double z, double t)> &func, const int coo)
426 {
427 clear();
428
429 tfunc_ = func;
430 tfunc_coo_ = coo;
431 }
432
434 {
435 if (t.is_array())
436 {
437 for (int i = 0; i < t.size(); ++i)
438 {
439 t_index_[std::round(t[i].get<double>() * 1000.) / 1000.] = i;
440 }
441
442 if (mat_.size() != t_index_.size() && mat_expr_.size() != t_index_.size())
443 logger().error("Specifying varying dirichlet over time, however 'time_reference' does not match dirichlet boundary conditions.");
444 }
445 }
446
447 double ExpressionValue::operator()(double x, double y, double z, double t, int index) const
448 {
449 double result;
450 if (expr_.empty())
451 {
452 if (t_index_.size() > 0)
453 {
454 t = std::round(t * 1000.) / 1000.;
455 if (t_index_.count(t) != 0)
456 {
457 if (mat_.size() > 0)
458 return mat_(t_index_.at(t));
459 else if (mat_expr_.size() > 0)
460 return mat_expr_[t_index_.at(t)](x, y, z, t, index);
461 }
462 else
463 {
464 logger().error("Cannot find dirichlet condition for time step {}.", t);
465 return 0;
466 }
467 }
468
469 if (mat_.size() > 0)
470 result = mat_(index);
471 else if (sfunc_)
472 result = sfunc_(x, y, z, t, index);
473 else if (tfunc_)
474 result = tfunc_(x, y, z, t)(tfunc_coo_);
475 else
476 result = value_;
477 }
478 else
479 {
480
481 std::vector<te_variable> vars = {
482 {"x", &x, TE_VARIABLE},
483 {"y", &y, TE_VARIABLE},
484 {"z", &z, TE_VARIABLE},
485 {"t", &t, TE_VARIABLE},
486 {"min", (const void *)min, TE_FUNCTION2},
487 {"max", (const void *)max, TE_FUNCTION2},
488 {"smoothstep", (const void *)smoothstep, TE_FUNCTION1},
489 {"half_smoothstep", (const void *)half_smoothstep, TE_FUNCTION1},
490 {"deg2rad", (const void *)deg2rad, TE_FUNCTION1},
491 {"rotate_2D_x", (const void *)rotate_2D_x, TE_FUNCTION3},
492 {"rotate_2D_y", (const void *)rotate_2D_y, TE_FUNCTION3},
493 {"if", (const void *)iflargerthanzerothenelse, TE_FUNCTION3},
494 {"compare", (const void *)compare, TE_FUNCTION2},
495 {"smooth_abs", (const void *)smooth_abs, TE_FUNCTION2},
496 {"sign", (const void *)sign, TE_FUNCTION1},
497 };
498
499 int err;
500 te_expr *tmp = te_compile(expr_.c_str(), vars.data(), vars.size(), &err);
501 if (!tmp)
502 {
503 logger().error("Unable to parse: {}", expr_);
504 logger().error("Error near character {}.", err);
505 log_and_throw_error("Invalid expression '{}'.", expr_);
506 }
507 result = te_eval(tmp);
508 te_free(tmp);
509 }
510
511 if (!unit_.base_units().empty())
512 {
513 if (!unit_.is_convertible(unit_type_))
514 log_and_throw_error(fmt::format("Cannot convert {} to {}", units::to_string(unit_), units::to_string(unit_type_)));
515
516 result = units::convert(result, unit_, unit_type_);
517 }
518
519 return result;
520 }
521 } // namespace utils
522} // namespace polyfem
double val
Definition Assembler.cpp:86
ElementAssemblyValues vals
Definition Assembler.cpp:22
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