-
Notifications
You must be signed in to change notification settings - Fork 10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Reconsider effects structure #387
Comments
AddendumLike meta, but bettaIf we go with the "separatist" approach and dataclasses, we might still retain something emulating the current No more implicit cmdsThe "regional config" that is the I thus propose phasing out any such "implicit" use of cmds via |
Another idea to harmonize both approaches by having a flag or something like that to indicate whether the bang-strings should be resolved or not. It should really not be necessary in the 'separatist' approach to track which parameters need to be resolved, because effectively, all of them need to be resolved, because all of them can be a bang-string. So any call to Having to wrap everything in What we maybe could do is have some flag to indicate whether we want to have the lookup done, and set this flag in the functions of the effect. E.g. through a decorator like I think we can follow such a scheme with both the Unionist and Separatist approach. For example by having the flag in the ChainMap, or by overloading |
A question about the unionist approach: if the user does I think |
One further point I forgot: Part of the original idea for the chain map was to have the (default) |
Yes I think that having all 'settings' in one place is a large value proposition of the unionist approach. The chainmap solution is rather beautiful, and worth having. Actually, I don't think there is a necessarily a discrepancy between the "chainmap" and "dataclass" approach (hence me renaming them from "unionist" and "separatist"). For me the appealing part of the dataclass approach is that the "kwargs" of an effect become Python attributes, which I like for various reasons. That benefit of dataclasses is kinda independent of the benefit of the chainmap, because the attributes can still (also) be in the chainmap. Perhaps we could get it to work like this:
The reason to have the defaults as in the last two bullets (and not the other way around) is because that makes typing easier:
I'm now thinking that such a attribute+chainmap approach could answer the to-resolve-or-not-to-resolve dilemma quite naturally: resolve when accessing as an attribute, do not resolve when accessing through the chainmap, unless explicitly asked for. A related thought is that this approach would fail when an E.g. if some effects needs a temperature, but it is realistically always the temperature of the instrument. Then it should still have this temperature in its I'm not saying this idea is necessarily the best approach, but I do feel that we are getting closer to consensus of what the best approach would be. |
Something like from dataclasses import dataclass, field
@dataclass
class Effect:
meta = None
def __post_init__(self):
# Populate self.meta.
setattr(self, "meta", {
k: getattr(self, k)
for k in self.__dataclass_fields__
})
def __getattribute__(self, k):
# Get self.meta in the usual way.
if k in {"meta"}:
return object.__getattribute__(self, k)
# Resolve values in self.meta through the chainmap.
if self.meta and k in self.meta:
return self.meta[k]
# Return any other value in the normal way.
return object.__getattribute__(self, k)
def __setattr__(self, k, v):
if k in {"meta"}:
return object.__setattr__(self, k, v)
if self.meta and k in self.meta:
self.meta[k] = v
return object.__setattr__(self, k, v)
@dataclass
class MyEffectA(Effect):
a: int = field(kw_only=True)
b: int = field(kw_only=True, default=5)
@dataclass
class MyEffectB(MyEffectA):
c: int = field(kw_only=True)
myeff = MyEffectB(a=4, c=3)
print(myeff.a)
print(myeff.meta)
myeff.meta['a'] = 9
print(myeff.a)
print(myeff.meta)
myeff.a = 7
print(myeff.a)
print(myeff.meta) Seems to work. Output:
|
I couldn't resist to do it with an actual from dataclasses import dataclass, field
from scopesim import UserCommands
from astar_utils import NestedMapping, RecursiveNestedMapping, NestedChainMap
from collections import abc, ChainMap
# Monkey patch NestedChainMap to make resolving optional. Kinda hacky.
NestedChainMap.old__getitem__ = NestedChainMap.__getitem__
NestedChainMap.resolve = False
NestedChainMap.__getitem__ = lambda self, key: self.old__getitem__(key) if self.resolve else ChainMap.__getitem__(self, key)
@dataclass
class Effect:
meta = None
cmds: UserCommands = None
def __post_init__(self):
# Populate self.meta.
themetadict = {
k: getattr(self, k)
for k in self.__dataclass_fields__
if k not in {"cmds", "title"}
}
themeta = NestedChainMap(
RecursiveNestedMapping(title="Meta"),
*[self.cmds],
)
themeta.update(themetadict)
setattr(self, "meta", themeta)
def __getattribute__(self, k):
# Get self.meta and self.cmds in the usual way.
if k in Effect.__dict__:
return object.__getattribute__(self, k)
# Resolve values in self.meta through the chainmap.
if self.meta and k in self.meta:
# Hackish way to only resolve when an attribute is used.
self.meta.resolve = True
v = self.meta[k]
self.meta.resolve = False
return v
# Return any other value in the normal way.
return object.__getattribute__(self, k)
def __setattr__(self, k, v):
if k in {"meta"}:
return object.__setattr__(self, k, v)
if self.meta and k in self.meta:
self.meta[k] = v
return object.__setattr__(self, k, v)
@dataclass
class MyEffectA(Effect):
a: int = field(kw_only=True)
temp: int = field(kw_only=True, default="!TEL.temperature")
@dataclass
class MyEffectB(MyEffectA):
c: int = field(kw_only=True)
cmds = UserCommands(packages=["test_package"],
yamls=["test_telescope.yaml",
{"alias": "ATMO",
"properties": {"pwv": 9001}}],
properties={"!ATMO.pwv": 8999})
myeff = MyEffectB(a=4, c=3, cmds=cmds)
print(f'{myeff.temp=}')
print(f'{myeff.meta["temp"]=}')
print(f'{myeff.meta["!TEL.temperature"]=}')
print()
cmds["!TEL.temperature"] = 888
print(f'{myeff.temp=}')
print(f'{myeff.meta["temp"]=}')
print(f'{myeff.meta["!TEL.temperature"]=}')
print()
myeff.temp = -20
print(f'{myeff.temp=}')
print(f'{myeff.meta["temp"]=}')
print(f'{myeff.meta["!TEL.temperature"]=}')
print() Prints out:
which is exactly what we expect. Good idea? Or bad idea? |
@hugobuddel I'm slowly getting back into this (after almost exactly 7 months 😛) because I think it might make resolving the whole AutoExposure / SummedExposure DIT, NDIT, exptime thingy (which is currently a major road block for me) a lot easier, as well as all kinds of other things. A few comments: The proposed solution used If we allow (not doing so would defeat the whole idea here) parameters to be (bang) strings, regardless of the type that the parameter would otherwise have, we should also reflect this in the typing of the parameter. I.e., using In terms of how to control resolving
I couldn't come up with a better one in 7 months 😄 |
Maybe just do class BangString(str):
... and take it from there? But we should allow the user to just specify a string, not require them instantiate a BangString. So we still need all that old logic, especially when the value can be a normal string too. The trailing ! seems like a quite elegant (or totally confusing?) way to resolve them, so let's just use that. I didn't know that existed either! I was thinking maybe pydantic could help us here, but I think that has other usecases. |
This is in preparation for solving AstarVienna/ScopeSim#387 A resolving string is a string key ending in `"!"`. An instance of `NestedChainMap` will only continue to follow references if the key is a resolving key, otherwise it will return the value a the bang-string that is stored.
This existed as an empty draft issue (aka "TODO") in the ScopeSim v1.0 project for some time, but now I found some content to put here, i.e. what actually should be discussed here.
This is a summary of a discussion between @hugobuddel, @astronomyk and @teutoburg held elsewhere.
Current state
The various subclasses of
Effect
are the flesh and guts of ScopeSim. It is thus considered vital to have a solid structure for them going forward. Currently, most effects rely on**kwargs
to accept parameters, usually from YAML files. Those parameters are stored in ameta
dict, along with a reference to the optical train's configuration incmds
, the latter being optional. Some effect subclasses have a set ofrequired_keys
that must be present in thekwargs
. Some effects are subclasses of other effects (seeter_curves.py
for some good examples), but may have different constructor signatures (i.e. a different set ofrequired_keys
) that gets converted for initializing the base class, with its own set of keys.Bang keys
Various parameters needed by an effect need to reference global (or "regional", i.e. optical train level) configurations, that may change after the effect instance is created, thus a "bang-string-key" is used to lookup those values in
cmds
. It is not always initially obvious for all effects, exactly which parameters might need lookup and which ones don't.Two ways forward
Unionist approach
meta
dict, but internally make this aChainMap
, that branches off of thecmds
one..cmds
as a separate attribute, and rather store everything inmeta
.ChainMap
would be a reference to the global and regional configurations.meta
as a bang-string-key would be automatically resolved by every__getitem__
call. This meansfrom_currsys(self.meta["!SIM.foo"], self.cmds)
would become redundant and simply be replaced withself.meta["!SIM.foo"]
, as the reference is resolved internally.unresolved
option somewhere, which is not ideal either...Separatist approach
meta
parameters into actual instance attributes.from_currsys
.The text was updated successfully, but these errors were encountered: