I am writing a robotics simulation using IsaacSim which I would like to encapsulate in a custom class to keep the simulation bits separate from the rest of my logic. However, the IsaacSim python API has an "extension" mechanism that requires the following import structure:
from isaacsim import SimulationApp
app = SimulationApp(some_params)
# other isaacsim imports
import omni.isaacsim.whatever
Where all the further imports (omni.isaacsim.whatever
) are only findable once the SimulationApp
instance has been created. Creating this instance takes a while (as it launches the IsaacSim simulator), and requires parameters that I need to set dynamically. My current code structure is as follows:
class Sim(object):
def __init__(self, params):
from isaacsim import SimulationApp
self.app = SimulationApp(params)
import omni.isaacsim.whatever
# etc.
def some_function(self):
# using omni.XY import
def other_func(self):
# also using omni.XY import
if __name__=='__main__':
# figure out initialization and params
...
sim = Sim(params)
# rest of the logic
sim.some_function()
...
My issue is in figuring out where to properly put the further omni.XY
imports. I need them in multiple functions of the Sim
class, however importing them in __init__
with import omni.XY
doesn't expose them in the other functions of Sim
, and putting the import statement for every omni.whatever
module in every function of Sim
doesn't seem right.
Is there some pattern that would respect the following constraints:
omni.whatever
imports must only happen after the SimulationApp
instance is created (ideally in Sim.__init__
)omni.whatever
imports need to be accessible by every function in the Sim
classSimulationApp
instance is only created when needed - i.e., when Sim
is createdIt is possible that creating Sim
as a class is not the correct solution and I'm open to other suggestions, although do keep in mind that I need to process some parameters to create the SimulationApp
instance.
Edit: the parameters for SimulationApp
are set in code based on other parts of the system (the module won't necessarily be run as the main script) and may not be known at the module level. Creating the SimulationApp
launches a heavy simulator and I need some control over when that happens as some other initialization needs to happen first. Creating this instance at the module level is impractical and would require to import the containing module in the middle of initialization code.
Python has somewhat simple mechanisms to control its variable and method scoping, and having access to them dynamically.
Once one gets a grasp of those, it is easy to deal with them.
I guess the best thing to to here, if __init__
will always run first than the other methods, is to expose then as module-global names so that they can be used from everywhere else in the same module, just like if it were done in a top-level import.
As it turns out, the import
statement just does a variable assignment in the scope where it occurs. Either import modulename
(equivalent to modulename = __import__("modulename")
) or from module import name
(name = __import__("module").name
)
So, all you need to have the imports inside __init__
to behave just like toplevel imports is to declare the top-most module name as a global variable, prior to the import.
I.E.:
class Sim(object):
def __init__(self, params):
global omni
from isaacsim import SimulationApp
self.app = SimulationApp(params)
import omni.isaacsim.whatever
Just adding the global omni
declaration there will make the omni
variable, as assigned by the import
statement further down, available for all functions and methods in the same module. The inner modules and packages are inner attriutes to the omni
object, so they will just work.
Showing another example with plain stdlib modules in a Python REPL session:
In [67]: def blah():
...: import math
...: ...
...:
In [68]: math
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[68], line 1
----> 1 math
NameError: name 'math' is not defined
In [69]: blah()
In [70]: math
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[70], line 1
----> 1 math
NameError: name 'math' is not defined
In [71]: def blah():
...: global math
...: import math
...: ...
...:
In [72]: blah()
In [73]: math
Out[73]: <module 'math' from '/home/jsbueno/.pyenv/versions/3.11.9/lib/python3.11/lib-dynload/math.cpython-311-x86_64-linux-gnu.so'>
omni
to an instance attributes:Otherwise, if you find interesting that in the other methods you can get to the imported modules by doing self.omni.XY
, it is possible to trick the import
statement to populate the self
namespace by running it inside an exec
- which allows one to especify the namespace where a command block is executed. So:
class Sim(object):
def __init__(self, params):
from isaacsim import SimulationApp
self.app = SimulationApp(params)
exec("import omni.isaacsim.whatever", vars(self))
...
def some_function(self):
# using omni.XY import:
self.omni.isaacsim.whatever.func(...)
...