From 2179861c13ea19c02b25df8edd2762890881fe5e Mon Sep 17 00:00:00 2001 From: Gemini Date: Fri, 7 Nov 2025 16:56:08 -0800 Subject: [PATCH 01/14] Beginning to refactor --- .gitignore | 3 + __pycache__/__init__.cpython-312.pyc | Bin 163 -> 0 bytes __pycache__/__init__.cpython-38.pyc | Bin 157 -> 0 bytes __pycache__/__init__.cpython-39.pyc | Bin 168 -> 0 bytes __pycache__/helper.cpython-312.pyc | Bin 5726 -> 0 bytes __pycache__/helper.cpython-38.pyc | Bin 3747 -> 0 bytes __pycache__/helper.cpython-39.pyc | Bin 3793 -> 0 bytes __pycache__/stochastic_tree.cpython-312.pyc | Bin 11275 -> 0 bytes __pycache__/stochastic_tree.cpython-38.pyc | Bin 7083 -> 0 bytes __pycache__/stochastic_tree.cpython-39.pyc | Bin 7086 -> 0 bytes examples/UFO_tie_prune_label.lpy | 20 +- stochastic_tree.py | 222 +++++++++++++++----- 12 files changed, 180 insertions(+), 65 deletions(-) delete mode 100644 __pycache__/__init__.cpython-312.pyc delete mode 100644 __pycache__/__init__.cpython-38.pyc delete mode 100644 __pycache__/__init__.cpython-39.pyc delete mode 100644 __pycache__/helper.cpython-312.pyc delete mode 100644 __pycache__/helper.cpython-38.pyc delete mode 100644 __pycache__/helper.cpython-39.pyc delete mode 100644 __pycache__/stochastic_tree.cpython-312.pyc delete mode 100644 __pycache__/stochastic_tree.cpython-38.pyc delete mode 100644 __pycache__/stochastic_tree.cpython-39.pyc diff --git a/.gitignore b/.gitignore index 592e1f4..fa570f4 100644 --- a/.gitignore +++ b/.gitignore @@ -164,3 +164,6 @@ cython_debug/ dataset*/ .DS_Store +GEMINI.md +.vscode/ +__* \ No newline at end of file diff --git a/__pycache__/__init__.cpython-312.pyc b/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 149509744fb380cbbcd73678d09d43030636ef34..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 163 zcmX@j%ge<81g5M<(?IlN5P=Rpvj9b=GgLBYGWxA#C}INgK7-W!a&oqc2`x@7Dvn7^ z%E-)%38+lYNG!??D9X=DO)e>p$tkFeFDXh*EzZnEVaLa3=4F<|$LkeT{^GF7%}*)K ZNwq6t1scf+#Kj=SM`lJw#v*1Q3jhZ8DcJx3 diff --git a/__pycache__/__init__.cpython-38.pyc b/__pycache__/__init__.cpython-38.pyc deleted file mode 100644 index 2b97dab352e73d6b300d4471a58d180f00288615..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 157 zcmWIL<>g`k0#nwbX(0MBh(HF6K#l_t7qb9~6oz01O-8?!3`HPe1o6ws*(xTqIJKxa zCNU`^GcP8fGC3o$C^w)eKPxr4q&Oz0pfbLsC^fY>GZ%#&AD@|*SrQ+wS5SG2!zMRB Pr8Fni4rKOcAZ7pnJjf?C diff --git a/__pycache__/__init__.cpython-39.pyc b/__pycache__/__init__.cpython-39.pyc deleted file mode 100644 index 08db88c9d2ee4a3bceee9b05d0cde2dc090ce08e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 168 zcmYe~<>g`k0#@r|X(0MBh(HF6K#l_t7qb9~6oz01O-8?!3`HPe1o6vPKeRZts8~NS zDI+s4u}t43wK%&Zzd%2|C^4nDq%O-rv5>cyamvzNtWC=^ni`5qwP}TC#^gm~I*6V~& zHLZG9d(N3N|2gNM|Ns8WIluS&JroqP{dXg)+9>MJ_`xYmy>tH~=)6zKluRe76dj>c zOoU0X5jMp|xD+4ZQ$j?b@p~reN{JCM<&L@g}PI2m3{DLg&dUq(5{r*C1Ewrw9mQm+26-G1;K%_MWlL|WN4 zVEOke6A1;8x!VT9jO9A4=2T5dS?;v*Mk*s`lL{WqrV}F>l(INgQRFaZi7;Osmtu;= zj3+EsO{6V0mQiDLU3yN#LgzjQ)%#SA4l{%ESiIM$Fm2HnRZJXu`22xq&%UZENIfeJ zk0#P*kLF^d5=tFKnKAej^=xuH7uArWsEO2){-}~1S5V)0&T>ns@nk~F$_iQzOS<5% z;yn$j6ub{DH5kKuI7NUC;p-xF-lueW?5kYExK3$}IWm2{PI0}C4*DBs$;<@IZko$J zYVL6=%uVzk7*%5DL$RzDRW+1I$D<=ClZvLKbYeW4l(a-99Ri_;U?3|cLkajuC8eY_ z^2VYM&SRz?((6D!B4|TF+CB|oV24@DGmb!Iuw+8EXw~AzC2ds29~26O&@yN(5$}sP zL?L)|!(ZJ3l|jvM{M#>0zEqg_Xixe0htJJ$n@ne<)H|hrbh@m5cy5N% z0)s$Uc4!q;59u=x>7hX|UN)(uEiNsk6omC|@zTXaCKYy}9!&cLu2$pm7GNxPUeTh% zQW7`>=Ylbl&ZaD%meHhS6xNArqm~z3MKo#q4Ha8o{X1-+_25Za@DOna=FJF4PXT=k z{MDa9Wl#$OwQRYu#q=#VHq0^Z!m-KC#s-t|6)sHn8JlK<9mQksE;lxtVq5X-ZL!yM zdkVX!R=u-&YIW(rZTD)^v$C|c>gg_Q`Pv(QcbUHJ?==JMbAg`Hg=!#F$e12);ZR|d zDYO=QCsW4QLObPdFP^Q5y$c9qC@aADFVbiopo_&802st* zHx~Vh^JWY?x~~-3p2Nz*^*S=uzx+otU9#Ai`NwAdY;GR4r(+RdUjx8t*JJsk$v3&l zHwoA#i9cX3zpY{PkU~bG2cDrW-#nczIIGH@|56^YZ!M35-%1|c4(E@Oo!l%B*P=X% z^ySvaOJtGun{k8~SO<^#-= zni9{UJ+N>X{%R6}fI*ppSO`wag?*E+6@!GfrlgBYQVp~nh)8u1Ix3DYpm1+%v2E%Z zW1Y!+3db*Ij*B=>U84uXH1Yx! zq?iJdX72uLhwqs9A4Y@nCUxX%}Fj!matbXUAlOg>%4I zGYc{;Gw@@ZJ&MduKqBV�lU>W=FUcO#%n#cN>m!X0bSdf)CoDwVn`yyEh3kbip_>p8r(kvdd z9#@Fc$r%Cd#j{wn57V&e;c+;}XiHFQwZ@fnG@VJP;Q2^`hA>=?z+e3hQ~(*@@{+r} zwS266;l{FQ_2$-V+3Vd`-@GYR);v?`-1Ei$L$~N#Yd_umH=gYHKr_WWQ!%Obeo5$|)O#d_YD7?vEDQ3k{8eBqXZFzh7 z!quJA!4LJD+i$8r-~GVF`ui6smhb292S<-NM7dq~fx^J5KI)|rMz_v*fpMp92xmrrqU2zewW zZwG6KbC3lbYV4x(&S>7R;gpsX-QO4u=m7}A{GbhEjNpUdr9?>g$MIP`EF!!X3gD_0 zS9lTP%q&3~9|t-Ff}#+XQnm#{n&hkMZe_g3BXYu4o#Vpm+d#ABI>&h5XAhZ*U$wgxc6oZt3%t2>h z4rt%88EBW^mKIl=dKYBE(k zRf<&UdTg3jZx1HoFrZS!gb{jSA1I`b1MFY4S_!{Xo@>ZQq|T)GICANbp#I5>vO4_BVQ z`~pX54O`MV=WX8IWD)Mhs7LqUOOHDCSHW`gScGus#5g4&M=#N(buJ<75ECRFpiYh0o-(gm;gEf%UT^viJ}s@ zEAtJLD={0pD*i$T=+N3&Z?9v$nG(f;avc8Z$526n<7%luhAR3BuU!tzE$cJ}XCb>h zRXTPhQtq4%UR^aUU45eF|Bi8R*56iXUsv<5zejO=dx186ZSMhHoob}oyQ8w_cCs_$5Vp7pgCPgQ(9KrSx7Blc9qo{!>XX*xK)ul7XWjJU-NwoM5OZdmbw zkLun~ezVrKtHQ1LqCHgU-C1kjWgIrWofWQ=z!^OcJcsFd>>k1ldI1_7f`{x!?BC}B zQ1EL0pI?6YCBm+2Fx+KzWWXs&aM92=4i^MkJlQ8DlO*oi8YJF|s1d`A=+g2v=D@`s zxql#cRybKBc^MYC?Ynr;Igl*+MB)yKW5m~d;trL#OFea$3f-kP)~Jp51d8su&vCT>zHbHX ky|<%_4&Hl~qg(FzXnO5^fusBH`&QB|4|cWCE66VY0?3IY9RL6T diff --git a/__pycache__/helper.cpython-38.pyc b/__pycache__/helper.cpython-38.pyc deleted file mode 100644 index 5577cb9eecc06d11a27284bfc7c37525c85acfbe..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3747 zcmZ`+TW=J}74E9OOwYyR0UT^>*mQ2YOoRvmCA&%pVHbj;1UYDTiJ}f@(w-`h8+Z4N zt7}*f>d8ZlvMAE7$RCKN_>} zmzEk9zJHzh=l1$(%lbDpj(-jsH&ODxg9uBo#7bGnQXcZu4(-$lozxB8)C)aE{XD6q ze(0z5ux{FR(nymq7<#0Legq`$McnWe?cu%eHw74LAQGd$9GvXc5 z5KV9^q9vBl_YKh&%c#$ajyQ#SReV#N5i1z6CIWF*tb#iyz9lxqd2s8Z_KDT~_6#~( zU9W6xj@4FvYpfp2vi2L<*F$x!Y~CBjnZEj=Qqg4IxK=e9_lC(Ni8FcicDeMCd=g7# z=3c$s;w)hY0X9NxV3;|KS1sqzQW)=r!~xHswVL@K?f zhL3Qh{6R9B^t6&PkJA(Uj!Z^UT^>!!dX$cmSdWFQVb%YjSZW1ivu_hW|A=={j9M4L z6zqjnSeglTz`o*a-WGh7WcHB^syk1v-`bJ=-Jn0#y0 zz)k@K<0uJY=p$2^Y131(J2SG!A^RRo>cfCi=?}H4L)Nv+#z?`+STYtR%S&ez>7BRc zQD-o!^l4|>lIG+dh{Zc>g{ceRPMne~Kz25#{1S{{Q@gOHPT>gtKGg_j)RxA157^W# ztis*3)Tm&EbIy8%lkakMqp-~=`;F9r^!Jc5)W6*uWj>Agz{1*?_fy!hT6HfdxNvuE z^=Hisj`cnCdp*1GKEZylrT64dx5hdTavdp+QwMaaAkKu`58fIg7-u?8f-Mzg{hgo@ z%s-ilAdd%#$ykbDYZAQo@Vx+L*E=$RwPPiNe7t3XC5X3=uV=Q)tIZM_38@shM6H4x zvfmxDhnt9QJCRxGWKk-WkB-uf_TypNt*LbyyFuhUNVp8e`nyv1wxR?Y%CJWM$;N5e z(nB33J*+d(J7E)dsh3n2Ytf|_Xv!EDXmAu#$D?T(+vM+pShm9)?z8IGMoonz9%~q> zhnD&dM)t|?M<3lgDkcst;jHiwTZNa-TJW%~R>&K|nQ~#nhjtYWyVi52F2ZB>Ie-^D z{X(;<^$6o2vxRqH=YL(0)`V}Q@G#AM1ivFr9QFLDEg0t3vEFlvAVi|pkN&hkaM+1bb!_Qo2W#odkUYxaMumxAWyt(NQiLA-t9UnfTUcCvOywyTT*`}DUu>J$juc+Z1Gr^wWLB1VCGnt zPJgV_Vpw#JE&)bHS`H>%ES!HI#9|$WSirB%TI>=#ji1lB(?R4 zo0+IMRM*VNN}=o2X`Tl^A>~GHd3MY4{;Mdc)XzZr1bs&zVJW3d6LNn8gUfF1)&UTJpaaI{VWDAh zBrjVyUpP}|U?DX+x2=aCBZXN=W*&KDS_6+{W_Vva1w-f(fYp!jjiOf6SFLHY@QViO zR^h&Ii)PW<=3imAy-TL;5-3tzdxeWcxuk2w66$u*h8NwY2rI)uee$m)^Wh5U229 zhHw(rs%iU2%v>_*%_WoGBy0i19?SW1rE6M?SN2?ANnn`?ok_1Bt3E)|jJ%JX@&JSY z!v};p%;zoEvQa4M@s8cLsYm@1V`g-)m)uxpMkjvBXRltrer-bTgv;qz^b#^Ie+K5r z{e`(!S_ssnXYmxzWr(GW5eWeG<DhB0Vm ifXcsK{dc-S$-tN-KqBn0HQVP6ud(i5@IBuKxA8x5GDuAT diff --git a/__pycache__/helper.cpython-39.pyc b/__pycache__/helper.cpython-39.pyc deleted file mode 100644 index 40c8eea20aa5d72ca08e1afabe8223ebbed59d09..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3793 zcmZ`+UvJ#T5$7&>Jl-EkXIZf=TXxu>`E!LJ8IFsjg=5sU>H>wU8cos$EQ~94yLX~R z^3KaWMA;I05;c|C&<~K9T zaA~P-;ky6Dzk1)Cv8;a+bMmTy`30WrpJ;?7SZpOMU6q@y9;7jte{^PEpZ0@Me(LME7maLlJLbjaURr$_>s6I zHb7k#)sLZ%`uZ)U!tso`c9MC& zWa{?@@i>lBd2Od?{8Ao8Qk63H0ljhQaG8E zqU{tOCd-ClR~CF26?PV-h20%wT}E2ZE~4qVjVE)^2!@UE-6;qC%xbfO9jZ0Zrg{Fy z?N2kQvh8qhKT5+#+dDEl(1YQ2Uxgymqh4=Y$xMc-yT2U|#~rPt%%bEJEf2$F7|U#X zYioO7#zU#LhU20}!bp#VtYWeM;IY(sG`n4!=-EXyfyZcj0YuKewQ}fu-qK9)WA+_q zkYNY*(T&^tvU}inN4k?~6{Y=7PYsez5~k5`6o)z*q(02$LofmZkM-;EY`@dyT0#!JXO= zZI&!;HuNA6tDCuPM%k~V z4y1pEl%fCZ;V|`R#D^B<#&;i)$;_+|{hW)+fvx_gc@FbUT)cNXcJ6*$n$bP_;`T^q zex^gEv1gz5Wl$yWq@yHg>VXd94(93W{h)zp>cnMeEjsiveKJA_ z_2+0h38WP)lf8q+vK{7dkCpc_dU{CWvbvGF;MCg~*(JZvFN!>R3=TH2*Amo%pR?d$ zTdfhOg)`y8h7au$4F}d!rrv|c>~&-~c=}t-O6~#1L1qj0*v`ILkXD6fr0_6(`2ckLJit2b2t+vv=B#dJB?bDNOsZ z?CeSXxg;cos-Kw<1-5vki)vgZ4rI-dE}ZU2sYO_{=0Sjwp_ct|8xv@DhB8!X+!H}yJ$4Bq2Rb} z&7NSN7p!A;34PD$^DnJ0S&s`xR33Yp3-<{$+QEAi0=pb5s>d9Zw+1YKMK-D^j4|VFd3p?E#st=$;XNnyj)7?Qbym^mQBwbz3 zUtDAtS?k5okNYz1q=P5}z)fD>x)Be$VVvCrG5=7}=B8xusX&7@T_O7@+J#Hd7ZMgD zJE)(dDJqz!FV#=Y$Won~^wJy;J|fB})>>TocX(ygv)Fswd*Rxv|5d-D;kVFqDecWK z%A+nG1Gj&GK%iSAN)AXr$BfM&p7?+%$ZJ5+d>1%_}I9OS+mb zp)4*Sa5O>= zZr_|#;b``?HSy4M^fkE6(G@tgmb-Zs)r=oI#-UedF08|ab-1vRH{e2h=0ZANz-wgb z(sesTQG!#yriP%ZNO?uax^^>ICI55>saAt{ZrJK!$HYld9nXoQ8R5~vyz?lGgXM*f z_)%7{$hcGe26G2%rw4RG`gsVNd*-*G^vI&`&dz*ya?B8OWfYahYutg~n#O+@xd*SQ z&+x{iamwUBBZix(+-+CXZ^0_c!(~HBX19H%SQ`MjAx>pmL!72`Ki&%CxXcoiNKP2Z zfSXRZ2I0nLiu_+RUrm9Z1ao#XXKSh56ww|>#P?75x|>eBxJ8%STR diff --git a/__pycache__/stochastic_tree.cpython-312.pyc b/__pycache__/stochastic_tree.cpython-312.pyc deleted file mode 100644 index 2c0d5d7de913825a56458cfc0aa78b806cdeeb8a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11275 zcmd5iTW}QFb^V(6NTbI{BMBi|f@PU45Kpu01B^f*v9JR1Mz9RcbW0j(G$VDlK$;yn z1J-(_>?RBqCk$S@EUHpvQ8*6%NW!L4mCZ*Sw(^l_YqvDpma9m46Nisf1aiu${c+B@ z{g^>$*RCWVxzgOnx#ym~efys0z5iHN=A|G_R{wo((>jX!1vaeYFe^*ng~}~TqSb3GhDxVWRo38f?M8gloz2^JUk^ms|1ug51kd;Cy$0hO|z zGJ_IBbx@*vk`g^q**(^HYELB#pP>hJ`u^bG|6=CXKe@jr%;;>}zWp*zI%-VP$g9o9(!W9F79~+VG^`&Z zu_8T5-7`CTI1hz?VuQ#^4v~|bq62$=USE0J=)7D1nqq0kN{=i4{`CK#TeKl0vL1#SyFF2~|L=Q4E&QsupVi z9u#XKuMtC#*NSzJhs1iw>&BVzYTaY6b(Ailk6Q`XXz33jy9KYT+Pwqj$1TC8($qb( z{{*#`Qthq*&aT>(b(Gvdm(p7SN2Jr#2h{f%i>gRnXRcCX>^bTx9cH>xm0d{{HcedO z)g&KH4v+I;R_CyyyG2PFCN;+aSx(BZ3)tl40R`vBe{pF4s~1iwlB`@1F89Y07mkfb z`vrOEn4BDtqN;KsK0F>#Wl2(EL(kL|H3^7{8jF(dR(Oyj8c!yqRKQ-qRsz6}rR;+& zLoITwW6wgM`o_fcMDD~~pgHeu{;vgJ;Kr-duRf({=qsrN#C=@*KX9}X)Ml%;5*u5B0LE8Ycxex`EOA7oz0eBUefXX=b_?I7)^6eKLw3sntvb8qgjT)P z8mGdJE*U?tlAk}BhC=sqMfIxGq* zjID~;ycDsvDJ%3zIvX8TWqeWHpBNpAT$Y7Iv|mzmA8gq;%n+sk+gOxFMkU>6^hU&3 zRF!c#boUTYmt#U)u7IXoi5af8jEgMe+J^l)r%3T$-9y%(S56LnDK42enGB&WZ$u)v z&#fcL%u&CbtPubb(f8JVs8;9{oz6Rx2UtOxL$g}Mf8 z))eYio2A(fHj?hpDg*}~1#3t%P=T}xfgmX=YoU0fPzRKN#6qZLKD0p#ZI}zaLaowc4q-;A39^x)baKr`pV(rqXPhvxE{&y&vR2Xx*i| z!VVc_fQ+n`g|jL2RfyFgwA z%~VtA26&#^x&TMn16hVzsH(lS>E@;x&pf|PZBYV@~Xe}6~9^fiZ~gQB> z@oljkK}_5S&%YijUBWjFPytm;4M4d(ouK4x01MOd3s5}wOs8Q0q$-L}H6T&ICPFMj zJpvOS7*$jlq+%bkU6{xl~UeKgTh?NIJ7K(P(e1GadVEW&JM*AL9)Jmes9Bo@jfb|su3T>skOxI&X4|DQ*C}ezFLp|-q+VXAV zCffw9&k~(t!3JT3f?6p<6*%|1o!2|3glmW2>BPq2>xZ+!`O38#{BvvHIgGGF*ABgN z`1!R00?!GH^T&x$0WL{i+UDH2a62l;qx zP~vUr@(k9Oa7Y<&5vXrd=chu&P5Vs8FwC}uXG%Ynt!w@gR`?behoCXgw8Ix9X=N@F z>A?*^lOD&9TuRRzB{HP{4d}HEPqPb(m1z#xM(Hz{wmJF?)HXz)fufPIEZswnghX^4 zGr=Yf{U6N9HfIgmm;=@S@to4E+z8{*09%#@Y*V+`k+vN_WDYP_*)AC+-oda}UJIqn zL#9`O6^Zt$DktNX!Ra7|r@R`GnlM8vQ(l8vGi16`8dJfB1wJF%8;ODAGb&$^bayOb zSf&cDC+cofGnvYr7`COFsT4jALzKUU44BSeo;gscTZa}x|+bC z1Hiiz*Cuj~pSo5T>Kf5KVG z;{2r{?b6U(UE*3YBTb)u6k0VOYSlumv+Dfjz1rrz^P3N9n-4zZ<~uKHofq?hG%v(7 zA(kIZ&JT`hgJW}{aU3~y_A^(2n3y5W8Jc%CY0jqH=%>yX7Fo(&VTb?}k5k!MD0EcI#9jUAb{$@lj z1ye_#XJ7@M5gk{+!YktKrqUdY0I@&8iRdg9-{5~m3v5^06@;mn$(o($8bEJkSz39` zME4Ip)_TEv+wNj^EPYdx;i3DH>vl0&pSS$gt7qv~*onSYDTSgR}X-ey^I8sjZfvKGkr7_*9kl3B> zhhRqXiUh1a3O*%14VlyAPU1kC15-6c?Lgd9gWk7bgz_(tfn8k|%$}UDS+CWspDTMY zb8x}so!WWh_378&+cW2C$VoqStzGcfX1_6`&cC=zdvVvC|CPM+l|pqebEJSK(eLjr zcq+0r*^!$e%~L<`Y0^AR`IeoUXJ^4*ojs6izS)uYHx}H%tS}vz@)v3wGUDV=!Bsvr zn(dyxs<}e*u13w(nCEwBt{nw$W%h*}ee=bg)sm+^%!^^O5=Uz1sQSxu(8Pxc-8#D*Jlgw70mikVH{3W7y9gus@i18L(|5WhV z4`)c$KY>vSj)(8Iy7~KW-F|C!$Di-IyK8Rs4wS781@vP-<$}f=+cSXy_8aen21cyF zq62ph*p5ztP)orcO0yzE>Hr5}!x9*i0tJUPU>P=bS>(YrLLUIaX6Q7Sp@xn8nAKs1 zH+U-$!e5oZDw4z!Y#tl~j9eeF#;8dZ{0+n8evIPmF;18G2c}xmX;G)gbo$6LO%y23 zAtI=eu~dC=*_L&E2S_R}K?b(GzidjF-k)vGcHdk#{W?Ss+HMCmU(*ad(|m`W5$?D& z--bN5!B|FQFDs!3Og2sfEH1H}97P?l;^u(`Mu`iHOG;c$W0?WMk{-q=i#;f%@ne|1 zqf<{J2{|tNRLH>FrSO8G=6P&+JhbqO(kfqboExUgWVR5l2u*_kG1gdt55iT zSM1+{XWOBKs=ovqp0YfNoCs$^RzwNbZ3t#kmM)tp2cZg<#qyN)rOVS!T$Qxb8Z{9} zJ65t7RNGEKAx2icJPqe^ShLqPV737USjN6btl~b$o`Bglu#7#ASj9bVU>R+w_u6Y~ ztuaK)k{p$Gq}{~2aGIus1jxjm_~mzQWBTyxeMaTU*TWv2Nen}jbp)cw9tonjk_>L0 z&ceZ+?hs^I7}uQwc#(aQJOE^%fpHj7cc`PoaVcCu>~-B~#*Wdx*IlOcH6qFEFvOdk zM+{ruLy8CxkdGlMJ)m>stb-HB##1^Yf^?0^{WxC|LO{QS7!@U%5qCHOJ*mc`oPFNs zm;nssKR}R!>F@Pt_AitLZk(Aula+F9H~Vu}XGU(PwA%1&n^v>we&|8vj~gBw_^A2A zPHk)ZM!Nlhq8*9kFGsb*QLQFAS0-i-7F=ahBiF{W z2f-pZl#^!KZue`!^|O^)^-Ht6?zi22O{?CixpqES|54?K;g1eHZ2r4W?bRa>N3>Ut z<-5=2d(La!7qnL{Xs!$SOTGENe(h2$Kak81k7xt3c1g~UUeO{~@>hRvk)qm|easUI z0wepexMURW{S0(sGHp1O6ip^J3^^l+s%{PIn#Xmm6_hz?mMx3U`wWRp=wpGbFS)zrxfcb zpLRlAbzQ+zmaTlRzEHc0e8(#@!tL=NR?nV>Nn7*sgL-Yveyw)@lsj8F?fcAK zw&;Svzj9Ol@*BIRcV#>0e5?PI`GNb6dp0!3Z`FKTGwll=-;KKIy6lT{o`&xp|NhzA zXJ@1jBARDoW*=^wbJORtN^W~jnJZiSl;Rw3(NlE6>&pm_yyf%WCe7QFQz77g<<8mJ zD|6nR8KL0xygP7h;M+AhM^3oyo~fMK1`7RacS2g-hS|#5t#^Z3-KJRy=G=dO5Ruo#5eQ|u~%IVsNngrV7bg!vb-2F$~G z-`#y}H^R3fyb|GCpE_B#0`|poM4nc7+4WBw%31y?&#;Y8-3%LE^4GBS#Ly%wf(vIS zD>ktskW5tf7LPr{ zE+cgHzST+Ig6J}x9@puVf@YYh$BZ3_J3Uo~&#(_3KL(Jp6*5p-6;*I{cc@T>2Y-hP z{>p0yGHn@Qie7g1Sf~x*Vcu8xm#sh#4<&xrs5%@19E2foP?3e=c?VGt@jM74%c%gF zeG!m{0ZIjAMv(jH^`qHtjjR2ZF}<#^Th1WKYnWY!On1RSUNRbq$dd?CF(U%t+gKxK zGewSl4`G=71N@YAkWEsbFXP*;%|b-3CC5v zvA$8*?@2g#!vH-ZDaK7H^gTiD!8sKmfLn_PRT3{HuEgYI0&j|JXnI#S2; zE@U7utIXqk4{k#PREE3+!5XUx#1n(05gAtPLwI6yd5u1-M{#dj2^k*QW2u&e7r~wQ za%RKZ45%ka0&}{Qjs>-Ep8zgr0^t%Iw2-YyA}veMfY~kYNIw(oQ&C|Um=HRH=XkAl zv9wHepj)IwuNcU)ohZ2PWQVy$MlIgpu;*+A?SLw}-6RzQ-=ID_094Gve)%>6(9f5@ z3z_a85(cG+`Mvdkc?-g9EAsac$NUob1I`KvYlu76QugfSW}3B7Fd~ zBu)j}D?y_Dup?vg191 zFA1BOIIy)@^myNI2CoSs2FCd;D<=VFdq-Hxv+lOP9141!0q+KPF_G(rRR2CS zQ$cHeOTYfVLXU#a1K}8`1nai4A-8j`lH3~DG3}UQrnW8A)aP1qmuJ?^w`|i|w$0US z&-72p*W-BcY{w&Cb;c5;o@Ihm{$rp^w#jlyit|}6NpB$xvpM*YDWN2Ndyy$;U(20b zq@b9MlH#Maq{yE>Ps+mTmPHnN3tPUngym8T#~Oh?LM~iM=7H{G)G*(Ia7qv7_>#~% z92XL5U%XX_$H_$w-2qZ-cw7c=oYL1mho(d&xF0GD!~LyMBj8K|+HipaqISt4osmTu zuj#gawpHbDnpsL_tCd2-cTKlV(Qw&|7Wq(Cs`Rgu+mkvFlAp3P)hz>rbAS0^g FzX7YL`vm|1 diff --git a/__pycache__/stochastic_tree.cpython-38.pyc b/__pycache__/stochastic_tree.cpython-38.pyc deleted file mode 100644 index e5d2fe17b441750d45ae86da4d0bdb3e682f28a7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7083 zcmcIpNsJuVd9JOitJmpSNX|$yU6v@CZP4RI1`OLW#Z3}q$Q{9?B)iR|Qd93uPt8iGkd5NrK!01i8Fh5Fi2%0g_Vy9qfGntLo{V zAszVQYSdqEU;h37``uQnuHkvK`VZR|FKOC;P-XG4P`Qpb{R;r5^);q5BhrUP-_WUS zM&@1mT9MVa)VI>N@vTJFq0@KN7%Qsv>#DEOZ!o*xM7fGJTK!hB&YK!@SnZLk}Sp)UPq0wJLt;t%bwNUFY>yFlKPhPLz=G$S+Q#ae;uD_LL!VfYxi2O8lZ~AE% zyq_fOYT599C@?5yW%bT@G)hFaS$~2{XwZ0B~8QjEejYry{4*YzUSGZmNswh{u z!x|se)cq>`8f&wqM_Rwm8@$OnAL(qFtvs?Yv&9?CR4uD)O|`W75_lCn$n_zbQtF&RjRx$L?S>DGuhgGtO-K( zCvV;Q`n`8kF4BAc)=n7Td*>k7@x|~Rk?isyOYcRa11}Str{VBaIn5FbOtUah-J95{ z5=2SNU!)t+)ilG zE4*<1u_*{qPY8%L0#xeOWM}B_ds}0cdMO_ad7Q~wwi5<>;N?`Vo)o<>4g^>It$|1$ zW;$E^wcCwIr{(hLsN+K8+xJVKuLRzsu+(AoQ zwJc?wqvd0ozmnGgSMxgHT2AYp%W19iIjwRbuK=#+HsFThm`YC}@{K3I!*jR$oLuuf zViABCN?;shiSRsGeGe>}27`U5V_V4R8PX?!yt54|nn z$H5LyWdmf2a6%jbq+|U4nD-k+ugAh56EDylXNc8B=ttrls^UC>3k22)Yyfnd(n@)> zEo%w~+aekM-m&W3>M&|`u}O=3iU3Jd0f>kqA^I+K#MH=*BlB3ht^MiRJ!5K4tqi;X zuCNYGUAwhc~qWv_c77T)fKDyaugg3!aTd{FIs`kyjTa$zwVxyvhB$J7MY;hQ?*3 z5ev6n--S|eABIunZVB%1xruONsK(Dcp74+h{-F=~Z{zkJ6%wVVmw-@n{SK z=*R9B*}4Pwfe+S!UQK)Il8^NQ5od_q3{%oZU`_0K7spH$mCVWc`K=d+{6Tj-T)ZP4 zsko;)7Q~B-*pFQ3;CKK7zT=NZJPzXl833~8Baw`_fPAHHvaMQ_S-)8$#PS=?HD4OcotWNDE?^%m~hp5K+ae*YbbD{^fY3@Au0y1KYR z>pyD~yHL@=$u`EMGvB7{T$BhZn|Kw&te}uVH)AvXkhG#fTwiQzN^=;-O8(XMU8A7v ztC(}Wz@QsASI!; zDYVizsQN5?I11^ovU_x#LcH2!V;3~VY{&wp+&|eOYyQ;1pCww~|BezvuvGHU_1&01 zgb=|hLZC?Rx(|0aqyth`2w1^*X(4k_lI*!rxX0ZSSv)oB|6!*V*ba)i)b1m?UMWKt z<^(@A=9DHR-9YG2bk9&2XwZOUJr%1lea{flhu#&s> zu?1}jeHmXo)eoC|x<5C?2yM<$Er+%(2--D!;v)8bqgxl3@fDW<+V~3+}@4C-6@34{6xR}iT(xY z+~L`qhA56PX7;X%c)Pgfa{>(PQB6!YqL2On~$V8X8jjF7_=SO3% zd}~E;CK zmU0@6-rYrSq2OD^vY*AYXR53E+`s!vj7bT78XTa>+UbK8Y#69+8iI$)C+E6^N)C2= zUIP>8tJ)eI8D^|%&uegsP1KBaw1N2MWQ)1*49Eh)!zYtjdWcuu{aHupmAW(k)qTjJ z<~Umz?ZPZlsQdP8PL^b_wuQlYLW=1L9ZpfG6jG@(sqahu?XzTCnjgaRGjIRrm{?kp z$;KQHXGR4ifwQNB0;L!fkuGC zs(oxum-5O{HFJoSafWt|R1RSc`z}!lUqwCDqj0TyP*OdFbDE2A^5qE;I76ITyX&#EKOu`WW^W4KajQ$ zvp?YC`QCH2+Y#QB3?`YioBE&YjtQpLWm&w63Yb0WQtV+n6Pm~<)-J| zANx_+(?q0{m|-G(HJu7-j?;2Fj^)^D-GC-9KBT&@<4wsOo~7`Z z-Z!~L={jb71Rce!M;4Nt6=ow@R%KNvZ%0~`W1L)ZZ@@W@LWU)re#BQ!xiwYjF+Dh^!vs^K8@XGm2NqrT-UsWM`TJ(L7285kVQ40+Do7)j9iOkAo&B=cj+59;2t!js7X_zKBoLG~Uu{&WY!OLYu@nvCdC%*y$1|HZ zvx&W(<)e-S7bI>7D3UF4AQ!}i0}=;L2ysI~9NrCasB%Dp6H*Vr@Be1jyXyqRg;~A- z`~LdB|M&W>R$arle*S#_ix)NRFI1WREL5%{rQZc$T2Es-Ga`N0=ovbd&B(k%xfNMG zOXZcGjl2?7cb%T2##m9US66+FUW3`aCdyT;(dxB|b#7_QVYLSutMS%>*;_!p&KjsU z4vgL+YE9Nct%X{fS+}*$!szAtO}-h%Jaw}z?)n>PCj20CgUC-)_qv~k!COhfE|(2& zg#v?OR#tBh2ZKap>-9&tl-AMZ!eoJ6o^2%zE&9j1fYlqf2N8DZfA6oqJp9LRfAM0+ zkmj}PH-v$C0ZpF$8mL@FN`C{uwVn=q8QjEejR)F+4*YzKSGZmNswh{u!y50^)cq>G z8e3qC542vLH+Yk^-_zL=TYg|+W{WqNsaj6471gr97ujicMy=cC?d>IYc2Z-jYz?a{ zW6o1*&N+5o%{j$B#4ey`g?*U0$WOD6u&0rqVHepm$j`EmvP;NUnF$;}D{IGi40POR z@=@7Ax`C8_7GTuOv_n1Dj`X**+nS|i+P1O(DP5x#r> z%Zqs2&$eWp7AVV37IM$u3sYH11mgl1NuoqZEB1GJ(8P|DkIKZ=>iK?+LavMDTsuU5 zq=R4aFqsaT#qYq(HP4uW(s zJdbDwAcjmB23aCJPgdXLRC-=k(##i`G1;d0q##^wR*EZRvI0 z0*5RZ3%Wi188c;!K<&6?w0w-6^~r((3Dn6)oJC1d=sF5Yyy)8yAY&sp4$UL&ruMa! zw~etmwldHKD8xE2b?xp7(ZoZ&+o^~u<_Th9IyGSqR8R>szMINw$}>O91V%kaqdp1H zYZQ;ChDv=$>i16EUVIqCUPnr801aZHt&c9uUgdGRgUGT4&xRs?OoEaSP!}X#WI9R1 z(cL>+Vd@q-$YrH|3pZWgg_v;fhf(Bi2=4E=iEv}c%}>1O7OO)GiXnG5(Z0{q%kHKh zr9~CP*4>cSy#XZ%rz-nB?e&j+b$9?GbEq^fJaTxbWJCLFuh-AP8crA64P1T|_ z{$)2E23xM50v`-K5AJcp82zOro%5TJM-#f~nXMfSu^>_TX><86#ld%8{0ImZ!%cN0kw%rXiYq%}6v4@g29MD@j{ zrZjiMSV;i2ea9$X_6wME;;Mq$wvdWfH6FX!@rEDApd3158+7^^&CtA}Mh7rd9VKGQ zLU}wix=PJ;EJ0#IZBxjl7pVFqq&tLJ4|+GE;}q<*K;Lm-mi4(;u3Q4 z41ipQ>LVqai6knuq^u}yDLz7jo+d!5QCuYOQ2=T4y$p&Ml%eDoeNeVq~3EChf-~ zzcFc{XenmRBd3K8|mph~Or1{TRpfk2=GiXjPC4OCT%DiSRsnNOGPTH5v?5ep=e{q_oSUF z@2S^>{hHk4AeSNN<-`Lk%c-3B9qgQvg`?qZ+fcFe%F8G_$NAIx==79zl<`(<`j{A? zhgT0xMp5CRkO_guflCHxDFf2z-kG%)3c9_B8RBCEo+Pg7Q(Nx`7?TqEG;$JF<_}ho zVW7Hc2tFzwpX(V^a**3O4Md=y(pF%|FyoYVPJ@|jqGqh34Ucb*Hkb>`fHWX%eo~pG zjdGsyne|-TNG3jX8!SXShbMv=K&P}1Mm@5V^$IKP!UN4m<*BYjmHHzsA3gIUl<&V7Yh4C%u^d~i5MYJ5kA(D9 zq?8QbNgR*qJ(F7$vt!14kVedUU?I|3VKyRWRaS+VcBDmt#?iCx&C+-C;g|6U*nuI5 z2&8gJxcmG~Hy!u^r%;y>GC}fBgaC7@f*e>niIR@w1%sc-?xSc3tdO^1lZ-24o1_6a z!&b4HdBs))nh;m?=vZ*IS+7wRi9m&mMb+lAK8-Qr6kHVAfD_pD8m+QI(6`g@FpLXO(bD3b&yH?A$AGG0J4gBKoWSK7*G= zh!vD%GG#7<2A3aMSd?xcrI!Ih_>Ay#fFNk^r^=|APBAn183xb^WAg>=ZZn5XQ$`Vl zhk37xEqd_)?e8NM5p?iN8wzZ+GCB1u!n>W?3HB5@W-5H1 z! solve with abstract classes growth_length = 0.1 basicwood_prototypes = {} -basicwood_prototypes['spur'] = Spur(tie_axis = (0,1,1), max_length = 0.1, thickness = 0.003, growth_length = growth_length/2, thickness_increment = 0., prototype_dict = basicwood_prototypes, color = [0, 255, 0]) -basicwood_prototypes['side_branch'] = LittleBranch(tie_axis = (1,1,0), max_length = 0.25, thickness = 0.003, growth_length =growth_length/2, thickness_increment = 0.00001, prototype_dict = basicwood_prototypes, color = [0, 255, 0]) +basicwood_prototypes['spur'] = Spur(tie_axis = (1, 0, 0), max_length = 0.1, thickness = 0.003, growth_length = growth_length/2, thickness_increment = 0., prototype_dict = basicwood_prototypes, color = [0, 255, 0]) +basicwood_prototypes['side_branch'] = LittleBranch(tie_axis = (0, 0, 1), max_length = 0.25, thickness = 0.003, growth_length =growth_length/2, thickness_increment = 0.00001, prototype_dict = basicwood_prototypes, color = [0, 255, 0]) -basicwood_prototypes['trunk'] = Trunk(tie_axis = (0,1,1), max_length = 3, thickness = 0.02, thickness_increment = 0.00001, growth_length = growth_length, prototype_dict = basicwood_prototypes, color = [255, 0, 0]) -basicwood_prototypes['branch'] = Branch(tie_axis = (1,1,0), max_length = 2.5, thickness = 0.01, thickness_increment = 0.00001, growth_length = growth_length, prototype_dict = basicwood_prototypes, color = [255, 150, 0]) +basicwood_prototypes['trunk'] = Trunk(tie_axis = (1, 0, 0), max_length = 3, thickness = 0.02, thickness_increment = 0.00001, growth_length = growth_length, prototype_dict = basicwood_prototypes, color = [255, 0, 0]) +basicwood_prototypes['branch'] = Branch(tie_axis = (0, 0, 1), max_length = 2.5, thickness = 0.01, thickness_increment = 0.00001, growth_length = growth_length, prototype_dict = basicwood_prototypes, color = [255, 150, 0]) #init trunk_base = Trunk(copy_from = basicwood_prototypes['trunk']) @@ -184,7 +184,7 @@ def generate_points_ufo(): -support = Support(generate_points_ufo(), 7 , 1 , (0.6,0,0.4), (0,1,1), (0,1,1)) +support = Support(generate_points_ufo(), 7 , 1 , (0.6,0,0.4)) num_iteration_tie = 8 num_iteration_prune = 16 bud_spacing_age = 2 @@ -285,7 +285,7 @@ module bud module branch module C curve = create_bezier_curve(x_range = (-1, 1), y_range = (-1, 1), z_range = (0, 10), seed_val=time.time()) -Axiom: Attractors(support)SetGuide(curve, trunk_base.max_length)[GetPos(trunk_base.start)T(ParameterSet(type = trunk_base))&(270)/(0)grow_object(trunk_base)GetPos(trunk_base.end)] +Axiom: Attractors(support)SetGuide(curve, trunk_base.max_length)[@GcGetPos(trunk_base.start)T(ParameterSet(type = trunk_base))&(270)/(0)grow_object(trunk_base)GetPos(trunk_base.end)] derivation length: 160 production: @@ -324,14 +324,14 @@ bud(t) : if 'LittleBranch' in new_object.name: import time curve = create_bezier_curve(x_range=(-.5, .5), y_range=(-.5, .5), z_range=(-1, 1), seed_val=time.time()) - nproduce[SetGuide(curve, new_object.max_length) + nproduce[@GcSetGuide(curve, new_object.max_length) elif 'Spur' in new_object.name: import time curve = create_bezier_curve(x_range=(-.2, .2), y_range=(-.2, .2), z_range=(-1, 1), seed_val=time.time()) - nproduce[SetGuide(curve, new_object.max_length) + nproduce[@GcSetGuide(curve, new_object.max_length) else: nproduce [ - nproduce @RGetPos(new_object.start)C(ParameterSet(type = new_object))/(rd.random()*360)&(rd.random()*360)grow_object(new_object)GetPos(new_object.end)]bud(t) + nproduce @RGetPos(new_object.start)C(ParameterSet(type = new_object))/(rd.random()*360)&(rd.random()*360)grow_object(new_object)GetPos(new_object.end)@Ge]bud(t) I(s,r,o) --> I(s,r+o.thickness_increment, o) diff --git a/stochastic_tree.py b/stochastic_tree.py index 95105a2..233f0cf 100644 --- a/stochastic_tree.py +++ b/stochastic_tree.py @@ -128,25 +128,67 @@ def update_guide(self, guide_target): curve, i_target = self.get_control_points(self.guide_target.point, self.start , self.end, self.tie_axis) else: curve, i_target= self.get_control_points(self.guide_target.point, self.last_tie_location , self.end, self.tie_axis) - if i_target: + if i_target is not None: self.guide_points.extend(curve) #self.last_tie_location = copy.deepcopy(Vector3(i_target)) #Replaced by updating location at StartEach + # def tie_lstring(self, lstring, index): + # #Lstring is the entire lstring + # #Index is where wood begins + # spline = CSpline(self.guide_points) + # if str(spline.curve()) == "nan": + # raise ValueError("CURVE IS NAN", self.guide_points) + # remove_count = 0 + # if not self.has_tied: + # if lstring[index+1].name in ['&','/','SetGuide']: + # del(lstring[index+1]) + # remove_count+=1 + # self.has_tied = True + # if lstring[index+1].name in ['&','/','SetGuide']: + # del(lstring[index+1]) + # remove_count+=1 + # lstring.insertAt(index+1, 'SetGuide({}, {})'.format(spline.curve(stride_factor = 100), self.length)) + # return lstring,remove_count + def tie_lstring(self, lstring, index): - spline = CSpline(self.guide_points) - if str(spline.curve()) == "nan": - raise ValueError("CURVE IS NAN", self.guide_points) - remove_count = 0 + """Insert a SetGuide(...) after position `index` in `lstring`. + + - Removes any immediate following tokens whose .name is in ('&','/','SetGuide'). + - Builds a CSpline from `self.guide_points` and inserts the curve string and length. + Returns (lstring, removed_count). + """ + # Nothing to do if we don't have guide points + if not self.guide_points: + return lstring, 0 + # Build spline and get curve representation (may raise) + try: + spline = CSpline(self.guide_points) + curve_repr = spline.curve(stride_factor=100) + except Exception as exc: + raise ValueError("Invalid spline from guide_points") from exc + + # Defensive check for 'nan' in the curve representation (preserve original check intent) + if "nan" in str(curve_repr): + raise ValueError("Curve is NaN", self.guide_points) + + # Remove any immediate tokens after index that match the removal set + removal_names = {"&", "/", "SetGuide"} + insert_pos = index + 1 + removed_count = 0 + + # Remove while the next token exists and matches + while insert_pos < len(lstring) and getattr(lstring[insert_pos], "name", None) in removal_names: + del lstring[insert_pos] + removed_count += 1 + + # Mark tied (if not already) if not self.has_tied: - if lstring[index+1].name in ['&','/','SetGuide']: - del(lstring[index+1]) - remove_count+=1 - self.has_tied = True - if lstring[index+1].name in ['&','/','SetGuide']: - del(lstring[index+1]) - remove_count+=1 - lstring.insertAt(index+1, 'SetGuide({}, {})'.format(spline.curve(stride_factor = 100), self.length)) - return lstring,remove_count + self.has_tied = True + + # Insert the new SetGuide token at the computed insert position + lstring.insertAt(insert_pos, f"SetGuide({curve_repr}, {self.length})") + + return lstring, removed_count def tie_update(self): self.last_tie_location = copy.deepcopy(self.end) @@ -159,27 +201,100 @@ def deflection_at_x(self,d, x, L): def get_control_points(self, target, start, current, tie_axis): - pts = [] - Lcurve = np.sqrt((start[0]-current[0])**2 + (current[1]-start[1])**2 + (current[2]-start[2])**2) - if Lcurve**2 - (target[0]-start[0])**2*tie_axis[0] - (target[1]-start[1])**2*tie_axis[1] - (target[2]-start[2])**2*tie_axis[2] <=0: - return pts,None - - curve_end = np.sqrt(Lcurve**2 - (target[0]-start[0])**2*tie_axis[0]-(target[1]-start[1])**2*tie_axis[1] - (target[2]-start[2])**2*tie_axis[2]) - - - i_target = [target[0], target[1], target[2]] - for j,axis in enumerate(tie_axis): - if axis == 0: - i_target[j] = start[j]+target[j]/abs(target[j])*(curve_end) - break - dxyz = np.array(i_target) - np.array(current) - dx = np.array(current) - np.array(start) - for i in np.arange(0.1,1.1,0.1): - x = i#/Lcurve#+1#/(10*(Lcurve)) + """ + Compute control points for a 3D curve from branch segment to tie point on wire. + + Uses vector projection to determine feasibility and compute the tie point location, + then generates a deflected curve using beam theory. + + Args: + target: Wire point (x, y, z) - a point on the wire + start: Branch segment start point (x, y, z) + current: Branch segment end point (x, y, z) + tie_axis: Unit direction vector of the wire (axis along which wire extends) + + Returns: + tuple: (control_points, tie_point) where: + - control_points: List of (x,y,z) tuples for curve fitting + - tie_point: Computed tie location on wire, or None if infeasible + + Geometry: + The branch, perpendicular offset to wire, and travel along wire form a right triangle: + - Hypotenuse = branch_length (||current - start||) + - One leg = perpendicular_distance (shortest distance from start to wire) + - Other leg = parallel_travel (distance to travel along wire to reach it) + """ + # Convert inputs to numpy arrays + start_arr = np.array([start[0], start[1], start[2]], dtype=float) + current_arr = np.array([current[0], current[1], current[2]], dtype=float) + wire_point = np.array([target[0], target[1], target[2]], dtype=float) + wire_axis = np.array(tie_axis, dtype=float) + + # Normalize wire axis to unit vector + wire_axis_norm = np.linalg.norm(wire_axis) + if wire_axis_norm < eps: + return [], None + wire_axis_unit = wire_axis / wire_axis_norm + + # Calculate branch segment length + segment_vector = current_arr - start_arr + branch_length = np.linalg.norm(segment_vector) + if branch_length < eps: + return [], None # Degenerate segment + + # Vector from branch start to wire point + v = wire_point - start_arr + + # Decompose v into components parallel and perpendicular to wire axis + parallel_component, perpendicular_component = self._get_parallel_and_perpendicular_components(v, wire_axis_unit) + perpendicular_distance = np.linalg.norm(perpendicular_component) + + # Feasibility check: branch must be long enough to reach the wire + if perpendicular_distance > branch_length: + return [], None + + # Calculate distance to travel along wire (Pythagorean theorem) + # branch_length² = perpendicular_distance² + parallel_travel² + parallel_travel_sq = branch_length**2 - perpendicular_distance**2 + parallel_travel = np.sqrt(max(0.0, parallel_travel_sq)) # Clamp to avoid floating-point negatives + + # Compute tie point on wire + # Start from perpendicular projection of start onto wire, then move parallel_travel along wire + start_projection_on_wire = start_arr + perpendicular_component + tie_point = start_projection_on_wire + parallel_travel * wire_axis_unit + + # Generate control points along deflected curve using beam deflection formula + control_points = self._generate_deflected_curve(start_arr, current_arr, tie_point) + + return control_points, tuple(tie_point) + + def _generate_deflected_curve(self, start, current, tie_point): + control_points = [] + deflection_vector = np.array(tie_point) - np.array(current) + branch_length = np.linalg.norm(np.array(current) - np.array(start)) + for step in np.arange(0.1, 1.1, 0.1): + # Parametric position along branch segment [0.1, 0.2, ..., 1.0] + t = step - d = self.deflection_at_x(dxyz, x*Lcurve, Lcurve) - pts.append(tuple((start[0]+x*dx[0]+d[0],start[1]+x*dx[1]+d[1],start[2]+x*dx[2]+d[2]))) - return pts, i_target + # Base position: linear interpolation from start to current + base_position = start + t * (current - start) + + # Add beam deflection (cantilever formula) + deflection = self.deflection_at_x(deflection_vector, t * branch_length, branch_length) + + # Combine base position and deflection + point = tuple(base_position + deflection) + control_points.append(point) + return control_points + + def _get_parallel_and_perpendicular_components(self, vec_a, vec_b): + # Project vec_a onto vec_b to get parallel and perpendicular components + vec_b_unit = vec_b / np.linalg.norm(vec_b) + parallel_component = np.dot(vec_a, vec_b_unit) * vec_b_unit + perpendicular_component = vec_a - parallel_component + return parallel_component, perpendicular_component + + # class Branch(BasicWood): # def __init__(self, num_buds_segment: int = 5, bud_break_prob: float = 0.8, thickness: float = 0.1,\ @@ -196,33 +311,30 @@ def get_control_points(self, target, start, current, tie_axis): # tie_axis: tuple = (0,1,1), bud_break_max_length: int = 5, order: int = 0, bud_break_prob_func: "Function" = lambda x,y: rd.random()): # super().__init__(num_buds_segment, bud_break_prob, thickness, thickness_increment, growth_length,\ # max_length, tie_axis, bud_break_max_length, order, bud_break_prob_func) - -class Wire(): - """ Defines a trellis wire in the 3D space """ - def __init__(self, id:int, point: tuple, axis: tuple): - self.__id = id - self.__axis = axis - x,y,z = point - self.point = Vector3(x,y,z) - self.num_branch = 0 - - def add_branch(self): - self.num_branch+=1 - + +from dataclasses import dataclass +from typing import Tuple + +@dataclass +class Wire: + # All wires are horizontal, tying axis depends on wood definition + id: int + point: Tuple[float,float,float] + num_branch: int = 0 + + def add_branch(self): + self.num_branch += 1 + class Support(): """ All the details needed to figure out how the support is structured in the environment, it is a collection of wires""" - def __init__(self, points: list, num_wires: int, spacing_wires: int, trunk_wire_pt: tuple,\ - branch_axis: tuple, trunk_axis: tuple): + def __init__(self, points: list, num_wires: int, spacing_wires: int, trunk_wire_pt: tuple,): self.num_wires = num_wires self.spacing_wires = spacing_wires - self.branch_axis = branch_axis self.branch_supports = self.make_support(points)#Dictionary id:points - self.trunk_axis = None self.trunk_wire = None - if trunk_axis: - self.trunk_axis = trunk_axis - self.trunk_wire = Wire(-1, trunk_wire_pt, self.trunk_axis) #Make it a vector? + if trunk_wire_pt: + self.trunk_wire = Wire(-1, trunk_wire_pt) points.append(trunk_wire_pt) self.attractor_grid = Point3Grid((1,1,1),list(points)) @@ -231,6 +343,6 @@ def __init__(self, points: list, num_wires: int, spacing_wires: int, trunk_wire_ def make_support(self, points): supports = {} for id,pt in enumerate(points): - supports[id] = Wire(id, pt, self.branch_axis) + supports[id] = Wire(id, pt) return supports From 99a1bc5526951153a653953fa7876400dabfe30f Mon Sep 17 00:00:00 2001 From: Gemini Date: Fri, 7 Nov 2025 17:58:56 -0800 Subject: [PATCH 02/14] Working Envy and UFO after refactor: --- examples/Camp_Envy_tie_prune_label.lpy | 50 ++-- examples/Envy_tie_prune_label.lpy | 58 ++-- examples/UFO_tie_prune_label.lpy | 60 ++-- stochastic_tree.py | 379 ++++++++++++++++++++----- tree_generation/make_n_trees.py | 2 +- 5 files changed, 395 insertions(+), 154 deletions(-) diff --git a/examples/Camp_Envy_tie_prune_label.lpy b/examples/Camp_Envy_tie_prune_label.lpy index 1804e6a..46eee04 100644 --- a/examples/Camp_Envy_tie_prune_label.lpy +++ b/examples/Camp_Envy_tie_prune_label.lpy @@ -246,12 +246,12 @@ def get_energy_mat(branches, arch): energy_matrix = np.ones((num_branches,num_wires))*np.inf #print(energy_matrix.shape) for branch_id, branch in enumerate(branches): - if branch.has_tied: + if branch.tying.has_tied: continue for wire_id, wire in arch.branch_supports.items(): if wire.num_branch>=1: continue - energy_matrix[branch_id][wire_id] = ed(wire.point,branch.end)/2+ed(wire.point,branch.start)/2#+v.num_branches*10+branch.bend_energy(deflection, curr_branch.age) + energy_matrix[branch_id][wire_id] = ed(wire.point,branch.location.end)/2+ed(wire.point,branch.location.start)/2#+v.num_branches*10+branch.bend_energy(deflection, curr_branch.info.age) return energy_matrix @@ -261,9 +261,9 @@ def decide_guide(energy_matrix, branches, arch): #print(min_arg) if(energy_matrix[min_arg[0][0]][min_arg[0][1]] == np.inf) or energy_matrix[min_arg[0][0]][min_arg[0][1]] > 1: return - if not (branches[min_arg[0][0]].has_tied == True):# and not (arch.branch_supports[min_arg[0][1]].num_branch >=1): + if not (branches[min_arg[0][0]].tying.has_tied == True):# and not (arch.branch_supports[min_arg[0][1]].num_branch >=1): #print("Imp:",min_arg[0][0], min_arg[0][1], energy_matrix[min_arg[0][0]][min_arg[0][1]]) - branches[min_arg[0][0]].guide_target = arch.branch_supports[min_arg[0][1]]#copy.deepcopy(arch.branch_supports[min_arg[0][1]].point) + branches[min_arg[0][0]].tying.guide_target = arch.branch_supports[min_arg[0][1]]#copy.deepcopy(arch.branch_supports[min_arg[0][1]].point) #trellis_wires.trellis_pts[min_arg[0][1]].num_branches+=1 for j in range(energy_matrix.shape[1]): energy_matrix[min_arg[0][0]][j] = np.inf @@ -273,13 +273,13 @@ def decide_guide(energy_matrix, branches, arch): def tie(lstring): for j,i in enumerate(lstring): if i == 'C' and i[0].type.__class__.__name__ == 'Branch': - if i[0].type.tie_updated == False: + if i[0].type.tying.tie_updated == False: continue curr = i[0] - if i[0].type.guide_points: - #print("tying ", i[0].type.name, i[0].type.guide_target.point) - i[0].type.tie_updated = False - i[0].type.guide_target.add_branch() + if i[0].type.tying.guide_points: + #print("tying ", i[0].type.name, i[0].type.tying.guide_target.point) + i[0].type.tying.tie_updated = False + i[0].type.tying.guide_target.add_branch() lstring, count = i[0].type.tie_lstring(lstring, j) return True @@ -289,7 +289,7 @@ def tie(lstring): def pruning_strategy(lstring): #Remove remnants of cut cut = False for j,i in enumerate(lstring): - if i.name == 'C' and i[0].type.age > 6 and i[0].type.has_tied == False and i[0].type.cut == False: + if i.name == 'C' and i[0].type.info.age > 6 and i[0].type.tying.has_tied == False and i[0].type.info.cut == False: i[0].type.cut = True #print("Cutting", i[0].type.name) @@ -300,7 +300,7 @@ def pruning_strategy(lstring): #Remove remnants of cut def StartEach(lstring): global parent_child_dict for i in parent_child_dict[trunk_base.name]: - if i.tie_updated == False: + if i.tying.tie_updated == False: i.tie_update() @@ -313,8 +313,8 @@ def EndEach(lstring): #print(energy_matrix) decide_guide(energy_matrix, parent_child_dict[trunk_base.name], support) for branch in parent_child_dict[trunk_base.name]: - branch.update_guide(branch.guide_target) - #print(branch.name, branch.guide_target) + branch.update_guide(branch.tying.guide_target) + #print(branch.name, branch.tying.guide_target) while tie(lstring): pass while pruning_strategy(lstring): @@ -483,21 +483,21 @@ grow_object(o) : if o == None: produce * else: - if o.length >= o.max_length: - o.age += 1 + if o.length >= o.growth.max_length: + o.info.age += 1 nproduce * else: # Apply color - r, g, b = o.color + r, g, b = o.info.color nproduce SetColor(r, g, b) # set unique color ID smallest_color = [r, g, b].index(min([r, g, b])) - o.color[smallest_color] += 1 + o.info.color[smallest_color] += 1 # Check if it's a trunk or branch, then apply the contour if 'Trunk' in o.name or 'Branch' in o.name: #print("TRUNK BRANCH NAME: " + o.name) - radius = o.thickness + radius = o.growth.thickness noise_factor = 0.8 num_points = 60 nproduce SetContour(o.contour) @@ -507,7 +507,7 @@ grow_object(o) : o.grow_one() if 'Spur' in o.name: - produce I(o.growth_length, o.thickness, o) bud(ParameterSet(type=o, num_buds=0)) spiked_bud(o.thickness)grow_object(o) + produce I(o.growth.growth_length, o.growth.thickness, o) bud(ParameterSet(type=o, num_buds=0)) spiked_bud(o.growth.thickness)grow_object(o) elif 'Leaf' in o.name: produce L(.1) elif 'Apple' in o.name: @@ -516,7 +516,7 @@ grow_object(o) : #produce [S(.1, .007)Ap] produce [S(.1/2, .09/15)f(.1)&(180)A(.1, .09)] else: - produce I(o.growth_length, o.thickness, o) bud(ParameterSet(type=o, num_buds=0)) grow_object(o) + produce I(o.growth.growth_length, o.growth.thickness, o) bud(ParameterSet(type=o, num_buds=0)) grow_object(o) #L(.1) bud(t) : @@ -538,13 +538,13 @@ bud(t) : # set a curve for tertiary branches to follow as they grow if 'NonTrunk' in new_object.name: import time - r, g, b = new_object.color + r, g, b = new_object.info.color seed = rgb_seed(r, g, b) curve = create_bezier_curve(seed=time.time()) - nproduce [SetGuide(curve, new_object.max_length) + nproduce [SetGuide(curve, new_object.growth.max_length) else: nproduce [ - nproduce @RGetPos(new_object.start)C(ParameterSet(type = new_object))/(rd.random()*360)&(rd.random()*90)grow_object(new_object)GetPos(new_object.end)]bud(t) + nproduce @RGetPos(new_object.location.start)C(ParameterSet(type = new_object))/(rd.random()*360)&(rd.random()*90)grow_object(new_object)GetPos(new_object.location.end)]bud(t) spiked_bud(r): base_height = r * 2 @@ -585,8 +585,8 @@ L(l): produce _(.0025) F(l/10){[SetGuide(curve1, l) _(.001).nF(l, .01)][SetGuide(curve2, l)_(.001).nF(l, .01)]} -I(s,r,o) --> I(s,r+o.thickness_increment, o) -#_(r) --> _(r+o.thickness_increment) +I(s,r,o) --> I(s,r+o.growth.thickness_increment, o) +#_(r) --> _(r+o.growth.thickness_increment) _(r) --> _(r) homomorphism: I(a,r,o) --> F(a,r) diff --git a/examples/Envy_tie_prune_label.lpy b/examples/Envy_tie_prune_label.lpy index eb085a0..22dcdcc 100644 --- a/examples/Envy_tie_prune_label.lpy +++ b/examples/Envy_tie_prune_label.lpy @@ -112,7 +112,7 @@ class Trunk(BasicWood): self.contour = create_noisy_circle_curve(3, 0.05, 30) def is_bud_break(self, num_buds_segment): - if (rd.random() > 0.1*(1 - num_buds_segment/self.max_buds_segment)): + if (rd.random() > 0.1*(1 - num_buds_segment/self.growth.max_buds_segment)): return False return True @@ -148,7 +148,7 @@ class NonTrunk(BasicWood): self.contour = create_noisy_circle_curve(1, .2, 30) def is_bud_break(self, num_buds_segment): - return (rd.random() < 0.02*(1 - num_buds_segment/self.max_buds_segment)) + return (rd.random() < 0.5*(1 - num_buds_segment/self.growth.max_buds_segment)) def create_branch(self): if rd.random()>0.3: @@ -166,10 +166,10 @@ growth_length = 0.1 bud_spacing_age = 2 #everything is relative to growth length basicwood_prototypes = {} -basicwood_prototypes['trunk'] = Trunk(tie_axis = (0,1,1), max_length = 4, thickness = 0.01, growth_length = growth_length,thickness_increment = 0.00001, prototype_dict = basicwood_prototypes, color = [255,0,0] ) -basicwood_prototypes['branch'] = Branch(tie_axis = (0,1,1), max_length = 2.2, thickness = 0.01, growth_length = growth_length,thickness_increment = 0.00001, prototype_dict = basicwood_prototypes, color = [255,150,0] ) -basicwood_prototypes['nontrunk'] = NonTrunk(tie_axis = (0,1,1), max_length = 0.3, growth_length = growth_length/2, thickness = 0.003,thickness_increment = 0.00001, prototype_dict = basicwood_prototypes, color = [0, 255, 0] ) -basicwood_prototypes['spur'] = Spur(tie_axis = (0,1,1), max_length = 0.2, thickness = 0.003, growth_length = growth_length/2, thickness_increment = 0., prototype_dict = basicwood_prototypes, color = [0,255,0] ) +basicwood_prototypes['trunk'] = Trunk(tie_axis = (0,1,0), max_length = 4, thickness = 0.01, growth_length = growth_length,thickness_increment = 0.00001, prototype_dict = basicwood_prototypes, color = [255,0,0] ) +basicwood_prototypes['branch'] = Branch(tie_axis = (1, 0, 0), max_length = 2.2, thickness = 0.01, growth_length = growth_length,thickness_increment = 0.00001, prototype_dict = basicwood_prototypes, color = [255,150,0] ) +basicwood_prototypes['nontrunk'] = NonTrunk(tie_axis = (1, 0, 0), max_length = 0.3, growth_length = growth_length/2, thickness = 0.003,thickness_increment = 0.00001, prototype_dict = basicwood_prototypes, color = [0, 255, 0] ) +basicwood_prototypes['spur'] = Spur(tie_axis = (1, 0, 0), max_length = 0.2, thickness = 0.003, growth_length = growth_length/2, thickness_increment = 0., prototype_dict = basicwood_prototypes, color = [0,255,0] ) #init @@ -191,7 +191,7 @@ def generate_points_v_trellis(): return pts # Return the list of points # points, num_wires spacing wires trunk_wire_pt branch_axis trunk_axis -support = Support(generate_points_v_trellis(), 14 , 1 , None, (0,0,1), None) +support = Support(generate_points_v_trellis(), 14 , 1 , None) num_iteration_tie = 5 num_iteration_prune = 16 ###Tying stuff begins @@ -208,14 +208,14 @@ def get_energy_mat(branches, arch): energy_matrix = np.ones((num_branches, num_wires)) * np.inf # Initialize the energy matrix with infinity for branch_id, branch in enumerate(branches): # Loop through each branch - if branch.has_tied or branch.cut: # If the branch is already tied, skip it + if branch.tying.has_tied or branch.info.cut: # If the branch is already tied, skip it continue for wire_id, wire in arch.branch_supports.items(): # Loop through each wire # print(f"ID {wire_id} num branch {wire.num_branch}") if wire.num_branch >= 1: # If the wire already has a branch, skip it continue # Calculate the energy required to tie the branch to the wire - energy_matrix[branch_id][wire_id] = ed(wire.point, branch.start) / 2 #ed(wire.point, branch.end) / 2 + + energy_matrix[branch_id][wire_id] = ed(wire.point, branch.location.start) / 2 #ed(wire.point, branch.location.end) / 2 + return energy_matrix # Return the energy matrix @@ -225,8 +225,8 @@ def decide_guide(energy_matrix, branches, arch): min_arg = np.argwhere(energy_matrix == np.min(energy_matrix)) # Find the minimum energy branch to tie if (energy_matrix[min_arg[0][0]][min_arg[0][1]] == np.inf) or energy_matrix[min_arg[0][0]][min_arg[0][1]] > 0.25*math.exp(getIterationNb()/105): return # If the minimum energy is too high, stop the process - if not branches[min_arg[0][0]].has_tied and not branches[min_arg[0][0]].cut: # If the branch has not been tied before - branches[min_arg[0][0]].guide_target = arch.branch_supports[min_arg[0][1]] # Set the guide target to the wire + if not branches[min_arg[0][0]].tying.has_tied and not branches[min_arg[0][0]].info.cut: # If the branch has not been tied before + branches[min_arg[0][0]].tying.guide_target = arch.branch_supports[min_arg[0][1]] # Set the guide target to the wire arch.branch_supports[min_arg[0][1]].add_branch() # Increment the number of branches tied to the wire # print(f"Tying {min_arg[0][0]} to wire {min_arg[0][1]}") # Set the energy to tie as infinite (only tie 1 branch per wire) @@ -239,11 +239,11 @@ def decide_guide(energy_matrix, branches, arch): def tie(lstring): for j, i in enumerate(lstring): # Loop through each element in the lstring if i == 'C' and i[0].type.__class__.__name__ == 'Branch': # Check if the element is a branch - if i[0].type.tie_updated == False: # If the branch is not updated, skip it + if i[0].type.tying.tie_updated == False: # If the branch is not updated, skip it continue curr = i[0] # Get the current branch - if i[0].type.guide_points: # If the branch has guide points - i[0].type.tie_updated = False # Set the tie_updated flag to False + if i[0].type.tying.guide_points: # If the branch has guide points + i[0].type.tying.tie_updated = False # Set the tie_updated flag to False #i[0].type.guide_target.add_branch() # Add the branch to the guide target lstring, count = i[0].type.tie_lstring(lstring, j) # Tie the branch @@ -254,7 +254,7 @@ def tie(lstring): def pruning_strategy(lstring): # Remove remnants of cut cut = False # Initialize the cut flag for j, i in enumerate(lstring): # Loop through each element in the lstring - if i.name == 'C' and i[0].type.age > 6 and i[0].type.has_tied == False and i[0].type.cut == False and i[0].type.prunable and i[0].type.guide_target==-1: + if i.name == 'C' and i[0].type.info.age > 6 and i[0].type.tying.has_tied == False and i[0].type.info.cut == False and i[0].type.info.prunable and i[0].type.tying.guide_target==-1: i[0].type.cut = True # Set the cut flag to True lstring = cut_from(j, lstring) # Cut the branch using string manipulation #TODO: Remove branch from parent_child dict and wire @@ -267,7 +267,7 @@ def pruning_strategy(lstring): # Remove remnants of cut def StartEach(lstring): global parent_child_dict for i in parent_child_dict[trunk_base.name]: # Loop through each child of the trunk base - if i.tie_updated == False: # If the branch is not updated + if i.tying.tie_updated == False: # If the branch is not updated i.tie_update() # Update the branch # Function to end each iteration @@ -278,7 +278,7 @@ def EndEach(lstring): energy_matrix = get_energy_mat(parent_child_dict[trunk_base.name], support) # Get the energy matrix decide_guide(energy_matrix, parent_child_dict[trunk_base.name], support) # Decide which branches to guide for branch in parent_child_dict[trunk_base.name]: # Loop through each branch - branch.update_guide(branch.guide_target) # Update the guide target for the branch + branch.update_guide(branch.tying.guide_target) # Update the guide target for the branch while tie(lstring): # Tie branches until no more can be tied pass if (getIterationNb() + 1) % num_iteration_prune == 0: @@ -309,7 +309,7 @@ def reset_contour(): curve = create_bezier_curve(x_range = (-1, 1), y_range = (-0.15, 0.15), z_range = (0, 10), seed_val=time.time()) -Axiom: Attractors(support)SetGuide(curve, trunk_base.max_length)[grow_object(trunk_base)] +Axiom: Attractors(support)SetGuide(curve, trunk_base.growth.max_length)[grow_object(trunk_base)] derivation length: 128 production: #Decide whether branch internode vs trunk internode need to be the same size. @@ -317,12 +317,12 @@ grow_object(o) : if o == None: produce * else: - if o.length >= o.max_length: - o.age += 1 + if o.length >= o.growth.max_length: + o.info.age += 1 nproduce * else: # Get object's usual color and apply it - r, g, b = o.color + r, g, b = o.info.color nproduce SetColor(r, g, b) # This sets unique color IDs @@ -342,11 +342,11 @@ grow_object(o) : if 'Spur' in o.name: # note that the production of the buds is here with 'spiked_bud(o.thickness)' - produce I(o.growth_length, o.thickness, o) bud(ParameterSet(type=o, num_buds=0))grow_object(o) #spiked_bud(o.thickness)grow_object(o) + produce I(o.growth.growth_length, o.growth.thickness, o) bud(ParameterSet(type=o, num_buds=0))grow_object(o) #spiked_bud(o.growth.thickness)grow_object(o) else: # If o is a Trunk, Branch, or NonTrunk, simply produce the internodes - nproduce I(o.growth_length, o.thickness, o) - if np.isclose(o.age%bud_spacing_age,0, atol = 0.01): + nproduce I(o.growth.growth_length, o.growth.thickness, o) + if np.isclose(o.info.age%bud_spacing_age,0, atol = 0.01): nproduce bud(ParameterSet(type=o, num_buds=0)) produce grow_object(o) @@ -367,14 +367,14 @@ bud(t) : if 'NonTrunk' in new_object.name: import time curve = create_bezier_curve(x_range = (-.5,.5), y_range = (-.5,.5), z_range = (-1,1), seed_val=time.time()) - nproduce [SetGuide(curve, new_object.max_length) + nproduce [SetGuide(curve, new_object.growth.max_length) elif 'Spur' in new_object.name: import time curve = create_bezier_curve(x_range = (-.2,.2), y_range = (-.2,.2), z_range = (-1,1), seed_val=time.time()) - nproduce [SetGuide(curve, new_object.max_length) + nproduce [SetGuide(curve, new_object.growth.max_length) else: nproduce [ - nproduce @RGetPos(new_object.start)C(ParameterSet(type = new_object))/(rd.random()*360)&(rd.random()*360)grow_object(new_object)GetPos(new_object.end)]bud(ParameterSet(type=t.type, num_buds=t.num_buds)) + nproduce @RGetPos(new_object.location.start)C(ParameterSet(type = new_object))/(rd.random()*360)&(rd.random()*360)grow_object(new_object)GetPos(new_object.location.end)]bud(ParameterSet(type=t.type, num_buds=t.num_buds)) # Simple set of productions to build apple bud. This bud is @@ -397,8 +397,8 @@ A(bh, r): produce nF(bh, .01, r, base_curve) ^(180) nF(bh/5, .1, r, top_curve)^(180)#S(bh/2,r/15) -I(s,r,o) --> I(s,r+o.thickness_increment, o) -#_(r) --> _(r+o.thickness_increment) +I(s,r,o) --> I(s,r+o.growth.thickness_increment, o) +#_(r) --> _(r+o.growth.thickness_increment) _(r) --> _(r) homomorphism: I(a,r,o) --> F(a,r) diff --git a/examples/UFO_tie_prune_label.lpy b/examples/UFO_tie_prune_label.lpy index 8378a84..caabfbc 100644 --- a/examples/UFO_tie_prune_label.lpy +++ b/examples/UFO_tie_prune_label.lpy @@ -33,7 +33,7 @@ class Spur(BasicWood): self.contour = create_noisy_circle_curve(1, .2, 30) def is_bud_break(self, num_buds_segment): - if num_buds_segment >= self.max_buds_segment: + if num_buds_segment >= self.growth.max_buds_segment: return False return (rd.random() < 0.1) @@ -66,7 +66,7 @@ class LittleBranch(BasicWood): def is_bud_break(self, num_buds_segment): if num_buds_segment >= 2: return False - if (rd.random() < 0.005*self.growth_length*(1 - self.num_buds/self.max_buds_segment)): + if (rd.random() < 0.005*self.growth.growth_length*(1 - self.num_buds/self.growth.max_buds_segment)): self.num_buds +=1 return True @@ -140,9 +140,9 @@ class Trunk(BasicWood): self.contour = create_noisy_circle_curve(1, .2, 30) def is_bud_break(self, num_buds_segment): - if num_buds_segment >= self.max_buds_segment: + if num_buds_segment >= self.growth.max_buds_segment: return False - if (rd.random() > 0.05*self.length/self.max_length*(1 - self.num_buds/self.max_buds_segment)): + if (rd.random() > 0.05*self.length/self.growth.max_length*(1 - self.num_buds/self.growth.max_buds_segment)): return False self.num_buds+=1 return True @@ -188,7 +188,7 @@ support = Support(generate_points_ufo(), 7 , 1 , (0.6,0,0.4)) num_iteration_tie = 8 num_iteration_prune = 16 bud_spacing_age = 2 -trunk_base.guide_target = support.trunk_wire +trunk_base.tying.guide_target = support.trunk_wire ###Tying stuff begins def ed(a,b): @@ -199,12 +199,12 @@ def get_energy_mat(branches, arch): num_wires = len(list(arch.branch_supports.values())) energy_matrix = np.ones((num_branches,num_wires))*np.inf for branch_id, branch in enumerate(branches): - if branch.has_tied: + if branch.tying.has_tied: continue for wire_id, wire in arch.branch_supports.items(): if wire.num_branch>=1: continue - energy_matrix[branch_id][wire_id] = ed(wire.point, branch.start)/2+ed(wire.point,branch.end)/2#,+v.num_branches*10+branch.bend_energy(deflection, curr_branch.age) + energy_matrix[branch_id][wire_id] = ed(wire.point, branch.location.start)/2+ed(wire.point,branch.location.end)/2#,+v.num_branches*10+branch.bend_energy(deflection, curr_branch.info.age) return energy_matrix def decide_guide(energy_matrix, branches, arch): @@ -212,8 +212,8 @@ def decide_guide(energy_matrix, branches, arch): min_arg = np.argwhere(energy_matrix == np.min(energy_matrix)) if energy_matrix[min_arg[0][0]][min_arg[0][1]] == np.inf or energy_matrix[min_arg[0][0]][min_arg[0][1]] > 1 : return - if not (branches[min_arg[0][0]].has_tied == True):# and not (arch.branch_supports[min_arg[0][1]].num_branch >=1): - branches[min_arg[0][0]].guide_target = arch.branch_supports[min_arg[0][1]]#copy.deepcopy(arch.branch_supports[min_arg[0][1]].point) + if not (branches[min_arg[0][0]].tying.has_tied == True):# and not (arch.branch_supports[min_arg[0][1]].num_branch >=1): + branches[min_arg[0][0]].tying.guide_target = arch.branch_supports[min_arg[0][1]]#copy.deepcopy(arch.branch_supports[min_arg[0][1]].point) arch.branch_supports[min_arg[0][1]].add_branch() # Increment the number of branches tied to the wire #trellis_wires.trellis_pts[min_arg[0][1]].num_branches+=1 @@ -225,12 +225,12 @@ def decide_guide(energy_matrix, branches, arch): def tie(lstring): for j,i in enumerate(lstring): if (i == 'C' and i[0].type.__class__.__name__ == 'Branch') or i == 'T' : - if i[0].type.tie_updated == False: + if i[0].type.tying.tie_updated == False: continue curr = i[0] - if i[0].type.guide_points: - i[0].type.tie_updated = False - i[0].type.guide_target.add_branch() + if i[0].type.tying.guide_points: + i[0].type.tying.tie_updated = False + i[0].type.tying.guide_target.add_branch() lstring, count = i[0].type.tie_lstring(lstring, j) return True @@ -239,11 +239,11 @@ def tie(lstring): def StartEach(lstring): global parent_child_dict, support, trunk_base - if support.trunk_wire and trunk_base.tie_updated == False: + if support.trunk_wire and trunk_base.tying.tie_updated == False: trunk_base.tie_update() for i in parent_child_dict[trunk_base.name]: - if i.tie_updated == False: + if i.tying.tie_updated == False: i.tie_update() @@ -254,11 +254,11 @@ def EndEach(lstring): if (getIterationNb()+1)%num_iteration_tie == 0: if support.trunk_wire : - trunk_base.update_guide(trunk_base.guide_target) #Tie trunk one iteration before branches + trunk_base.update_guide(trunk_base.tying.guide_target) #Tie trunk one iteration before branches energy_matrix = get_energy_mat(parent_child_dict[trunk_base.name], support) decide_guide(energy_matrix, parent_child_dict[trunk_base.name], support) for branch in parent_child_dict[trunk_base.name]: - branch.update_guide(branch.guide_target) + branch.update_guide(branch.tying.guide_target) while tie(lstring): pass if (getIterationNb() + 1) % num_iteration_prune == 0: @@ -269,8 +269,8 @@ def EndEach(lstring): def pruning_strategy(lstring): #Remove remnants of cut cut = False for j,i in enumerate(lstring): - if i.name == 'C' and i[0].type.age > 8 and i[0].type.has_tied == False and i[0].type.cut == False: - i[0].type.cut = True + if i.name == 'C' and i[0].type.info.age > 8 and i[0].type.tying.has_tied == False and i[0].type.info.cut == False: + i[0].type.info.cut = True lstring = cut_from(j, lstring) return True return False @@ -285,7 +285,7 @@ module bud module branch module C curve = create_bezier_curve(x_range = (-1, 1), y_range = (-1, 1), z_range = (0, 10), seed_val=time.time()) -Axiom: Attractors(support)SetGuide(curve, trunk_base.max_length)[@GcGetPos(trunk_base.start)T(ParameterSet(type = trunk_base))&(270)/(0)grow_object(trunk_base)GetPos(trunk_base.end)] +Axiom: Attractors(support)SetGuide(curve, trunk_base.growth.max_length)[@GcGetPos(trunk_base.location.start)T(ParameterSet(type = trunk_base))&(270)/(0)grow_object(trunk_base)GetPos(trunk_base.location.end)] derivation length: 160 production: @@ -293,19 +293,19 @@ production: grow_object(o) : if o == None: produce * - if o.length >= o.max_length: + if o.length >= o.growth.max_length: nproduce * else: nproduce SetContour(o.contour) o.grow_one() if label: - r, g, b = o.color + r, g, b = o.info.color nproduce SetColor(r,g,b) if 'Spur' in o.name: - produce I(o.growth_length, o.thickness, o)bud(ParameterSet(type = o, num_buds = 0))@O(o.thickness*1.2)grow_object(o) + produce I(o.growth.growth_length, o.growth.thickness, o)bud(ParameterSet(type = o, num_buds = 0))@O(o.growth.thickness*1.2)grow_object(o) else: - nproduce I(o.growth_length, o.thickness, o) - if np.isclose(o.age%bud_spacing_age,0, atol = 0.01): + nproduce I(o.growth.growth_length, o.growth.thickness, o) + if np.isclose(o.info.age%bud_spacing_age,0, atol = 0.01): nproduce bud(ParameterSet(type = o, num_buds = 0)) produce grow_object(o) #produce I(o.growth_length, o.thickness, o)bud(ParameterSet(type = o, num_buds = 0))grow_object(o) @@ -324,18 +324,18 @@ bud(t) : if 'LittleBranch' in new_object.name: import time curve = create_bezier_curve(x_range=(-.5, .5), y_range=(-.5, .5), z_range=(-1, 1), seed_val=time.time()) - nproduce[@GcSetGuide(curve, new_object.max_length) + nproduce[@GcSetGuide(curve, new_object.growth.max_length) elif 'Spur' in new_object.name: import time curve = create_bezier_curve(x_range=(-.2, .2), y_range=(-.2, .2), z_range=(-1, 1), seed_val=time.time()) - nproduce[@GcSetGuide(curve, new_object.max_length) + nproduce[@GcSetGuide(curve, new_object.growth.max_length) else: nproduce [ - nproduce @RGetPos(new_object.start)C(ParameterSet(type = new_object))/(rd.random()*360)&(rd.random()*360)grow_object(new_object)GetPos(new_object.end)@Ge]bud(t) + nproduce @RGetPos(new_object.location.start)C(ParameterSet(type = new_object))/(rd.random()*360)&(rd.random()*360)grow_object(new_object)GetPos(new_object.location.end)@Ge]bud(t) -I(s,r,o) --> I(s,r+o.thickness_increment, o) -_(r) --> _(r+o.thickness_increment) +I(s,r,o) --> I(s,r+o.growth.thickness_increment, o) +_(r) --> _(r+o.growth.thickness_increment) homomorphism: diff --git a/stochastic_tree.py b/stochastic_tree.py index 233f0cf..860c6ce 100644 --- a/stochastic_tree.py +++ b/stochastic_tree.py @@ -47,32 +47,21 @@ def __init__(self, copy_from = None, max_buds_segment: int = 5, thickness: float if copy_from: self.__copy_constructor__(copy_from) return - self.start = Vector3(0,0,0) - self.end = Vector3(0,0,0) + self.location = LocationState() #Tying variables - self.last_tie_location = Vector3(0,0,0) - self.has_tied = False - self.guide_points = [] + self.tying = TyingState(tie_axis=tie_axis) self.current_tied = False - self.guide_target = -1#Vector3(0,0,0) - self.tie_axis = tie_axis - self.tie_updated = False #Information Variables - self.__length = 0 - self.age = 0 - self.cut = False - self.prunable = True - self.order = order - self.num_branches = 0 - self.branch_dict = collections.deque() - self.color = color - self.material = material + self.info = InfoState(order=order, color=color, material=material) + self.__length = 0 #Growth Variables - self.max_buds_segment = max_buds_segment - self.thickness = thickness - self.thickness_increment = thickness_increment - self.growth_length = growth_length - self.max_length = max_length + self.growth = GrowthState( + max_buds_segment=max_buds_segment, + thickness=thickness, + thickness_increment=thickness_increment, + growth_length=growth_length, + max_length=max_length + ) @@ -81,6 +70,220 @@ def __copy_constructor__(self, copy_from): for k,v in update_dict.items(): setattr(self, k, v) #self.__dict__.update(update_dict) + + # ===== Location property accessors for backward compatibility ===== + @property + def start(self): + """Get the start location.""" + return self.location.start + + @start.setter + def start(self, value): + """Set the start location.""" + self.location.start = value + + @property + def end(self): + """Get the end location.""" + return self.location.end + + @end.setter + def end(self, value): + """Set the end location.""" + self.location.end = value + + @property + def last_tie_location(self): + """Get the last tie location.""" + return self.location.last_tie_location + + @last_tie_location.setter + def last_tie_location(self, value): + """Set the last tie location.""" + self.location.last_tie_location = value + + # ===== Tying property accessors for backward compatibility ===== + @property + def has_tied(self): + """Get whether this wood object has been tied.""" + return self.tying.has_tied + + @has_tied.setter + def has_tied(self, value): + """Set whether this wood object has been tied.""" + self.tying.has_tied = value + + @property + def guide_points(self): + """Get the guide control points list.""" + return self.tying.guide_points + + @guide_points.setter + def guide_points(self, value): + """Set the guide control points list.""" + self.tying.guide_points = value + + @property + def guide_target(self): + """Get the guide target (Wire object or -1).""" + return self.tying.guide_target + + @guide_target.setter + def guide_target(self, value): + """Set the guide target.""" + self.tying.guide_target = value + + @property + def tie_axis(self): + """Get the tie axis direction vector.""" + return self.tying.tie_axis + + @tie_axis.setter + def tie_axis(self, value): + """Set the tie axis direction vector.""" + self.tying.tie_axis = value + + @property + def tie_updated(self): + """Get whether the tie has been updated.""" + return self.tying.tie_updated + + @tie_updated.setter + def tie_updated(self, value): + """Set whether the tie has been updated.""" + self.tying.tie_updated = value + + # ===== Growth property accessors for backward compatibility ===== + @property + def max_buds_segment(self): + """Get the maximum buds per segment.""" + return self.growth.max_buds_segment + + @max_buds_segment.setter + def max_buds_segment(self, value): + """Set the maximum buds per segment.""" + self.growth.max_buds_segment = value + + @property + def thickness(self): + """Get the current thickness.""" + return self.growth.thickness + + @thickness.setter + def thickness(self, value): + """Set the current thickness.""" + self.growth.thickness = value + + @property + def thickness_increment(self): + """Get the thickness increment per step.""" + return self.growth.thickness_increment + + @thickness_increment.setter + def thickness_increment(self, value): + """Set the thickness increment per step.""" + self.growth.thickness_increment = value + + @property + def growth_length(self): + """Get the growth length per step.""" + return self.growth.growth_length + + @growth_length.setter + def growth_length(self, value): + """Set the growth length per step.""" + self.growth.growth_length = value + + @property + def max_length(self): + """Get the maximum total length.""" + return self.growth.max_length + + @max_length.setter + def max_length(self, value): + """Set the maximum total length.""" + self.growth.max_length = value + + # ===== Info property accessors for backward compatibility ===== + @property + def age(self): + """Get the age of this wood object.""" + return self.info.age + + @age.setter + def age(self, value): + """Set the age of this wood object.""" + self.info.age = value + + @property + def cut(self): + """Get whether this wood object has been cut.""" + return self.info.cut + + @cut.setter + def cut(self, value): + """Set whether this wood object has been cut.""" + self.info.cut = value + + @property + def prunable(self): + """Get whether this wood object is prunable.""" + return self.info.prunable + + @prunable.setter + def prunable(self, value): + """Set whether this wood object is prunable.""" + self.info.prunable = value + + @property + def order(self): + """Get the hierarchical order of this wood object.""" + return self.info.order + + @order.setter + def order(self, value): + """Set the hierarchical order of this wood object.""" + self.info.order = value + + @property + def num_branches(self): + """Get the number of branches from this wood object.""" + return self.info.num_branches + + @num_branches.setter + def num_branches(self, value): + """Set the number of branches from this wood object.""" + self.info.num_branches = value + + @property + def branch_dict(self): + """Get the branch dictionary/deque.""" + return self.info.branch_dict + + @branch_dict.setter + def branch_dict(self, value): + """Set the branch dictionary/deque.""" + self.info.branch_dict = value + + @property + def color(self): + """Get the color code for this wood object.""" + return self.info.color + + @color.setter + def color(self, value): + """Set the color code for this wood object.""" + self.info.color = value + + @property + def material(self): + """Get the material code for this wood object.""" + return self.info.material + + @material.setter + def material(self, value): + """Set the material code for this wood object.""" + self.info.material = value @abstractmethod def is_bud_break(self) -> bool: @@ -111,7 +314,7 @@ def grow_one(self): self.grow() @abstractmethod - def create_branch(self) -> "BasicWood Object": + def create_branch(self): """Returns how a new order branch when bud break happens will look like if a bud break happens""" pass #new_object = BasicWood.clone(self.branch_object) @@ -120,36 +323,33 @@ def create_branch(self) -> "BasicWood Object": # self.max_length/2, self.tie_axis, self.bud_break_max_length/2, self.order+1, self.bud_break_prob_func) def update_guide(self, guide_target): - curve = [] + """Compute and append guide control points for this wood object. + + Args: + guide_target: Wire object (with .point attribute) or None/-1 (no-op). + + Notes: + - If infeasible (tie point cannot be reached), silently returns. + - Appends control points incrementally to self.guide_points. + - Uses self.start as base if not yet tied; self.last_tie_location otherwise. + """ self.guide_target = guide_target - if self.guide_target == -1: + if guide_target is None or guide_target == -1: return - if self.has_tied == False: - curve, i_target = self.get_control_points(self.guide_target.point, self.start , self.end, self.tie_axis) - else: - curve, i_target= self.get_control_points(self.guide_target.point, self.last_tie_location , self.end, self.tie_axis) - if i_target is not None: + + # Select base point: use last tie location if already tied, otherwise start + base_point = self.last_tie_location if self.has_tied else self.start + + # Compute control points and tie point in one call + curve, tie_point = self.get_control_points( + guide_target.point, base_point, self.end, self.tie_axis + ) + + # Append only if feasible (tie_point is not None) + if tie_point is not None and curve: self.guide_points.extend(curve) - #self.last_tie_location = copy.deepcopy(Vector3(i_target)) #Replaced by updating location at StartEach - - # def tie_lstring(self, lstring, index): - # #Lstring is the entire lstring - # #Index is where wood begins - # spline = CSpline(self.guide_points) - # if str(spline.curve()) == "nan": - # raise ValueError("CURVE IS NAN", self.guide_points) - # remove_count = 0 - # if not self.has_tied: - # if lstring[index+1].name in ['&','/','SetGuide']: - # del(lstring[index+1]) - # remove_count+=1 - # self.has_tied = True - # if lstring[index+1].name in ['&','/','SetGuide']: - # del(lstring[index+1]) - # remove_count+=1 - # lstring.insertAt(index+1, 'SetGuide({}, {})'.format(spline.curve(stride_factor = 100), self.length)) - # return lstring,remove_count - + # Note: last_tie_location updated at StartEach hook, not here + def tie_lstring(self, lstring, index): """Insert a SetGuide(...) after position `index` in `lstring`. @@ -175,7 +375,7 @@ def tie_lstring(self, lstring, index): removal_names = {"&", "/", "SetGuide"} insert_pos = index + 1 removed_count = 0 - + # Remove while the next token exists and matches while insert_pos < len(lstring) and getattr(lstring[insert_pos], "name", None) in removal_names: del lstring[insert_pos] @@ -261,7 +461,9 @@ def get_control_points(self, target, start, current, tie_axis): # Compute tie point on wire # Start from perpendicular projection of start onto wire, then move parallel_travel along wire start_projection_on_wire = start_arr + perpendicular_component - tie_point = start_projection_on_wire + parallel_travel * wire_axis_unit + direction_to_wire = np.sign(np.dot(wire_point, wire_axis_unit)) + tie_point = start_projection_on_wire + parallel_travel * wire_axis_unit * direction_to_wire + # Generate control points along deflected curve using beam deflection formula control_points = self._generate_deflected_curve(start_arr, current_arr, tie_point) @@ -294,27 +496,66 @@ def _get_parallel_and_perpendicular_components(self, vec_a, vec_b): perpendicular_component = vec_a - parallel_component return parallel_component, perpendicular_component - - -# class Branch(BasicWood): -# def __init__(self, num_buds_segment: int = 5, bud_break_prob: float = 0.8, thickness: float = 0.1,\ -# thickness_increment: float = 0.01, growth_length: float = 1., max_length: float = 7.,\ -# tie_axis: tuple = (0,1,1), bud_break_max_length: int = 5, order: int = 0, bud_break_prob_func: "Function" = lambda x,y: rd.random()): -# super().__init__(num_buds_segment, bud_break_prob, thickness, thickness_increment, growth_length,\ -# max_length, tie_axis, bud_break_max_length, order, bud_break_prob_func) - - -# class Trunk(BasicWood): -# """ Details of the trunk while growing a tree, length, thickness, where to attach them etc """ -# def __init__(self, num_buds_segment: int = 5, bud_break_prob: float = 0.8, thickness: float = 0.1,\ -# thickness_increment: float = 0.01, growth_length: float = 1., max_length: float = 7.,\ -# tie_axis: tuple = (0,1,1), bud_break_max_length: int = 5, order: int = 0, bud_break_prob_func: "Function" = lambda x,y: rd.random()): -# super().__init__(num_buds_segment, bud_break_prob, thickness, thickness_increment, growth_length,\ -# max_length, tie_axis, bud_break_max_length, order, bud_break_prob_func) from dataclasses import dataclass from typing import Tuple +@dataclass +class LocationState: + """Location tracking for a wood object: start point, end point, and last tie location.""" + start: any = None # Vector3 + end: any = None # Vector3 + last_tie_location: any = None # Vector3 + + def __post_init__(self): + """Initialize Vector3 points if not provided.""" + if self.start is None: + self.start = Vector3(0, 0, 0) + if self.end is None: + self.end = Vector3(0, 0, 0) + if self.last_tie_location is None: + self.last_tie_location = Vector3(0, 0, 0) + +@dataclass +class TyingState: + """Tying and guiding state for a wood object.""" + has_tied: bool = False + guide_points: list = None # List of (x,y,z) tuples for spline control points + guide_target: any = -1 # Wire object or -1 (no target) + tie_axis: tuple = (0, 1, 1) # Direction vector for the wire axis + tie_updated: bool = False + + def __post_init__(self): + """Initialize guide_points as empty list if not provided.""" + if self.guide_points is None: + self.guide_points = [] + +@dataclass +class GrowthState: + """Growth parameters for a wood object.""" + max_buds_segment: int = 5 + thickness: float = 0.1 + thickness_increment: float = 0.01 + growth_length: float = 1.0 + max_length: float = 7.0 + +@dataclass +class InfoState: + """Information/metadata for a wood object.""" + age: int = 0 + cut: bool = False + prunable: bool = True + order: int = 0 + num_branches: int = 0 + color: int = 0 + material: int = 0 + branch_dict: any = None # collections.deque + + def __post_init__(self): + """Initialize branch_dict if not provided.""" + if self.branch_dict is None: + self.branch_dict = collections.deque() + @dataclass class Wire: # All wires are horizontal, tying axis depends on wood definition diff --git a/tree_generation/make_n_trees.py b/tree_generation/make_n_trees.py index c53fd4d..ee2a372 100644 --- a/tree_generation/make_n_trees.py +++ b/tree_generation/make_n_trees.py @@ -11,7 +11,7 @@ parser = argparse.ArgumentParser() parser.add_argument('--num_trees', type=int, default=1) parser.add_argument('--output_dir', type=str, default='dataset/') - parser.add_argument('--lpy_file', type=str, default='examples/UFO_tie_prune_label.lpy') + parser.add_argument('--lpy_file', type=str, default='examples/Envy_tie_prune_label.lpy') parser.add_argument('--verbose', action='store_true', default=False) args = parser.parse_args() num_trees = args.num_trees From f63e750fa0b8f46c7a50832099c8427d09d9acd8 Mon Sep 17 00:00:00 2001 From: Gemini Date: Mon, 10 Nov 2025 12:04:26 -0800 Subject: [PATCH 03/14] remove properties for direct data class access --- examples/Camp_Envy_tie_prune_label.lpy | 10 +- examples/Envy_tie_prune_label.lpy | 10 +- examples/UFO_tie_prune_label.lpy | 4 +- stochastic_tree.py | 240 ++----------------------- tree_generation/make_n_trees.py | 3 +- 5 files changed, 31 insertions(+), 236 deletions(-) diff --git a/examples/Camp_Envy_tie_prune_label.lpy b/examples/Camp_Envy_tie_prune_label.lpy index 46eee04..b260c36 100644 --- a/examples/Camp_Envy_tie_prune_label.lpy +++ b/examples/Camp_Envy_tie_prune_label.lpy @@ -122,7 +122,7 @@ class Branch(BasicWood): self.contour = create_noisy_circle_curve(1, .2, 30) def is_bud_break(self, num_buds_segment): - return (rd.random() < 0.01*(1 - self.num_buds_segment/self.max_buds_segment)) + return (rd.random() < 0.01*(1 - self.num_buds_segment/self.growth.max_buds_segment)) def create_branch(self): self.num_buds_segment += 1 @@ -154,7 +154,7 @@ class Trunk(BasicWood): self.contour = create_noisy_circle_curve(1, .2, 30) def is_bud_break(self, num_buds_segment): - if (rd.random() > 0.2*(1 - num_buds_segment/self.max_buds_segment)): + if (rd.random() > 0.2*(1 - num_buds_segment/self.growth.max_buds_segment)): return False return True @@ -186,7 +186,7 @@ class NonTrunk(BasicWood): self.contour = create_noisy_circle_curve(1, .2, 30) def is_bud_break(self, num_buds_segment): - return (rd.random() < 0.02*(1 - num_buds_segment/self.max_buds_segment)) + return (rd.random() < 0.02*(1 - num_buds_segment/self.growth.max_buds_segment)) def create_branch(self): if rd.random()>0.9: @@ -291,7 +291,7 @@ def pruning_strategy(lstring): #Remove remnants of cut for j,i in enumerate(lstring): if i.name == 'C' and i[0].type.info.age > 6 and i[0].type.tying.has_tied == False and i[0].type.info.cut == False: - i[0].type.cut = True + i[0].type.info.cut = True #print("Cutting", i[0].type.name) lstring = cut_using_string_manipulation(j, lstring) return True @@ -528,7 +528,7 @@ bud(t) : parent_child_dict[t.type.name].append(new_object) #Store new object somewhere t.num_buds+=1 - t.type.num_branches+=1 + t.type.info.num_branches+=1 # set a different cross section for every branch if 'Leaf' not in new_object.name and 'Apple' not in new_object.name: diff --git a/examples/Envy_tie_prune_label.lpy b/examples/Envy_tie_prune_label.lpy index 22dcdcc..70fefb5 100644 --- a/examples/Envy_tie_prune_label.lpy +++ b/examples/Envy_tie_prune_label.lpy @@ -36,7 +36,7 @@ class Spur(BasicWood): # Keep count of number of leaves to not go over max_leaves self.num_leaves = 0 - self.prunable = False + self.info.prunable = False # Every branch gets its own contour when it is constructed to ensure # branches each having a unique profile curve @@ -77,7 +77,7 @@ class Branch(BasicWood): def is_bud_break(self, num_break_buds): if num_break_buds >= 1: return False - return (rd.random() < 0.5*(1 - self.num_buds_segment/self.max_buds_segment)) + return (rd.random() < 0.5*(1 - self.num_buds_segment/self.growth.max_buds_segment)) def create_branch(self): self.num_buds_segment += 1 @@ -141,7 +141,7 @@ class NonTrunk(BasicWood): if not name: self.name = str(self.__class__.__name__) +'_'+ str(self.__class__.count) Branch.count+=1 - self.prunable = False + self.info.prunable = False # Every branch gets its own contour when it is constructed to ensure # branches each having a unique profile curve @@ -255,7 +255,7 @@ def pruning_strategy(lstring): # Remove remnants of cut cut = False # Initialize the cut flag for j, i in enumerate(lstring): # Loop through each element in the lstring if i.name == 'C' and i[0].type.info.age > 6 and i[0].type.tying.has_tied == False and i[0].type.info.cut == False and i[0].type.info.prunable and i[0].type.tying.guide_target==-1: - i[0].type.cut = True # Set the cut flag to True + i[0].type.info.cut = True # Set the cut flag to True lstring = cut_from(j, lstring) # Cut the branch using string manipulation #TODO: Remove branch from parent_child dict and wire #if i[0].type.guide_target!=-1: @@ -361,7 +361,7 @@ bud(t) : parent_child_dict[t.type.name].append(new_object) #Store new object somewhere t.num_buds+=1 - t.type.num_branches+=1 + t.type.info.num_branches+=1 # Set a curve for tertiary branches to follow as they grow if 'NonTrunk' in new_object.name: diff --git a/examples/UFO_tie_prune_label.lpy b/examples/UFO_tie_prune_label.lpy index caabfbc..7a2063d 100644 --- a/examples/UFO_tie_prune_label.lpy +++ b/examples/UFO_tie_prune_label.lpy @@ -102,7 +102,7 @@ class Branch(BasicWood): def is_bud_break(self, num_buds_segment): if num_buds_segment >= 2: return False - if (rd.random() < 0.2*(1 - self.num_buds/self.max_buds_segment)): + if (rd.random() < 0.2*(1 - self.num_buds/self.growth.max_buds_segment)): return True @@ -319,7 +319,7 @@ bud(t) : parent_child_dict[t.type.name].append(new_object) #Store new object somewhere t.num_buds+=1 - t.type.num_branches+=1 + t.type.info.num_branches+=1 if 'LittleBranch' in new_object.name: import time diff --git a/stochastic_tree.py b/stochastic_tree.py index 860c6ce..8dbda36 100644 --- a/stochastic_tree.py +++ b/stochastic_tree.py @@ -72,219 +72,13 @@ def __copy_constructor__(self, copy_from): #self.__dict__.update(update_dict) # ===== Location property accessors for backward compatibility ===== - @property - def start(self): - """Get the start location.""" - return self.location.start - - @start.setter - def start(self, value): - """Set the start location.""" - self.location.start = value - - @property - def end(self): - """Get the end location.""" - return self.location.end - - @end.setter - def end(self, value): - """Set the end location.""" - self.location.end = value - - @property - def last_tie_location(self): - """Get the last tie location.""" - return self.location.last_tie_location - - @last_tie_location.setter - def last_tie_location(self, value): - """Set the last tie location.""" - self.location.last_tie_location = value # ===== Tying property accessors for backward compatibility ===== - @property - def has_tied(self): - """Get whether this wood object has been tied.""" - return self.tying.has_tied - - @has_tied.setter - def has_tied(self, value): - """Set whether this wood object has been tied.""" - self.tying.has_tied = value - - @property - def guide_points(self): - """Get the guide control points list.""" - return self.tying.guide_points - - @guide_points.setter - def guide_points(self, value): - """Set the guide control points list.""" - self.tying.guide_points = value - - @property - def guide_target(self): - """Get the guide target (Wire object or -1).""" - return self.tying.guide_target - - @guide_target.setter - def guide_target(self, value): - """Set the guide target.""" - self.tying.guide_target = value - - @property - def tie_axis(self): - """Get the tie axis direction vector.""" - return self.tying.tie_axis - - @tie_axis.setter - def tie_axis(self, value): - """Set the tie axis direction vector.""" - self.tying.tie_axis = value - - @property - def tie_updated(self): - """Get whether the tie has been updated.""" - return self.tying.tie_updated - - @tie_updated.setter - def tie_updated(self, value): - """Set whether the tie has been updated.""" - self.tying.tie_updated = value # ===== Growth property accessors for backward compatibility ===== - @property - def max_buds_segment(self): - """Get the maximum buds per segment.""" - return self.growth.max_buds_segment - - @max_buds_segment.setter - def max_buds_segment(self, value): - """Set the maximum buds per segment.""" - self.growth.max_buds_segment = value - - @property - def thickness(self): - """Get the current thickness.""" - return self.growth.thickness - - @thickness.setter - def thickness(self, value): - """Set the current thickness.""" - self.growth.thickness = value - - @property - def thickness_increment(self): - """Get the thickness increment per step.""" - return self.growth.thickness_increment - - @thickness_increment.setter - def thickness_increment(self, value): - """Set the thickness increment per step.""" - self.growth.thickness_increment = value - - @property - def growth_length(self): - """Get the growth length per step.""" - return self.growth.growth_length - - @growth_length.setter - def growth_length(self, value): - """Set the growth length per step.""" - self.growth.growth_length = value - - @property - def max_length(self): - """Get the maximum total length.""" - return self.growth.max_length - - @max_length.setter - def max_length(self, value): - """Set the maximum total length.""" - self.growth.max_length = value # ===== Info property accessors for backward compatibility ===== - @property - def age(self): - """Get the age of this wood object.""" - return self.info.age - - @age.setter - def age(self, value): - """Set the age of this wood object.""" - self.info.age = value - @property - def cut(self): - """Get whether this wood object has been cut.""" - return self.info.cut - - @cut.setter - def cut(self, value): - """Set whether this wood object has been cut.""" - self.info.cut = value - - @property - def prunable(self): - """Get whether this wood object is prunable.""" - return self.info.prunable - - @prunable.setter - def prunable(self, value): - """Set whether this wood object is prunable.""" - self.info.prunable = value - - @property - def order(self): - """Get the hierarchical order of this wood object.""" - return self.info.order - - @order.setter - def order(self, value): - """Set the hierarchical order of this wood object.""" - self.info.order = value - - @property - def num_branches(self): - """Get the number of branches from this wood object.""" - return self.info.num_branches - - @num_branches.setter - def num_branches(self, value): - """Set the number of branches from this wood object.""" - self.info.num_branches = value - - @property - def branch_dict(self): - """Get the branch dictionary/deque.""" - return self.info.branch_dict - - @branch_dict.setter - def branch_dict(self, value): - """Set the branch dictionary/deque.""" - self.info.branch_dict = value - - @property - def color(self): - """Get the color code for this wood object.""" - return self.info.color - - @color.setter - def color(self, value): - """Set the color code for this wood object.""" - self.info.color = value - - @property - def material(self): - """Get the material code for this wood object.""" - return self.info.material - - @material.setter - def material(self, value): - """Set the material code for this wood object.""" - self.info.material = value - @abstractmethod def is_bud_break(self) -> bool: """This method defines if a bud will break or not -> returns true for yes, false for not. Input can be any variables""" @@ -306,11 +100,11 @@ def length(self): @length.setter def length(self, length): - self.__length = min(length, self.max_length) + self.__length = min(length, self.growth.max_length) def grow_one(self): - self.age+=1 - self.length+=self.growth_length + self.info.age+=1 + self.length+=self.growth.growth_length self.grow() @abstractmethod @@ -330,46 +124,46 @@ def update_guide(self, guide_target): Notes: - If infeasible (tie point cannot be reached), silently returns. - - Appends control points incrementally to self.guide_points. - - Uses self.start as base if not yet tied; self.last_tie_location otherwise. + - Appends control points incrementally to self.tying.guide_points. + - Uses self.location.start as base if not yet tied; self.location.last_tie_location otherwise. """ - self.guide_target = guide_target + self.tying.guide_target = guide_target if guide_target is None or guide_target == -1: return # Select base point: use last tie location if already tied, otherwise start - base_point = self.last_tie_location if self.has_tied else self.start + base_point = self.location.last_tie_location if self.tying.has_tied else self.location.start # Compute control points and tie point in one call curve, tie_point = self.get_control_points( - guide_target.point, base_point, self.end, self.tie_axis + guide_target.point, base_point, self.location.end, self.tying.tie_axis ) # Append only if feasible (tie_point is not None) if tie_point is not None and curve: - self.guide_points.extend(curve) + self.tying.guide_points.extend(curve) # Note: last_tie_location updated at StartEach hook, not here def tie_lstring(self, lstring, index): """Insert a SetGuide(...) after position `index` in `lstring`. - Removes any immediate following tokens whose .name is in ('&','/','SetGuide'). - - Builds a CSpline from `self.guide_points` and inserts the curve string and length. + - Builds a CSpline from `self.tying.guide_points` and inserts the curve string and length. Returns (lstring, removed_count). """ # Nothing to do if we don't have guide points - if not self.guide_points: + if not self.tying.guide_points: return lstring, 0 # Build spline and get curve representation (may raise) try: - spline = CSpline(self.guide_points) + spline = CSpline(self.tying.guide_points) curve_repr = spline.curve(stride_factor=100) except Exception as exc: raise ValueError("Invalid spline from guide_points") from exc # Defensive check for 'nan' in the curve representation (preserve original check intent) if "nan" in str(curve_repr): - raise ValueError("Curve is NaN", self.guide_points) + raise ValueError("Curve is NaN", self.tying.guide_points) # Remove any immediate tokens after index that match the removal set removal_names = {"&", "/", "SetGuide"} @@ -382,8 +176,8 @@ def tie_lstring(self, lstring, index): removed_count += 1 # Mark tied (if not already) - if not self.has_tied: - self.has_tied = True + if not self.tying.has_tied: + self.tying.has_tied = True # Insert the new SetGuide token at the computed insert position lstring.insertAt(insert_pos, f"SetGuide({curve_repr}, {self.length})") @@ -391,8 +185,8 @@ def tie_lstring(self, lstring, index): return lstring, removed_count def tie_update(self): - self.last_tie_location = copy.deepcopy(self.end) - self.tie_updated = True + self.location.last_tie_location = copy.deepcopy(self.location.end) + self.tying.tie_updated = True def deflection_at_x(self,d, x, L): """d is the max deflection, x is the current location we need deflection on and L is the total length""" diff --git a/tree_generation/make_n_trees.py b/tree_generation/make_n_trees.py index ee2a372..ee18e65 100644 --- a/tree_generation/make_n_trees.py +++ b/tree_generation/make_n_trees.py @@ -11,7 +11,7 @@ parser = argparse.ArgumentParser() parser.add_argument('--num_trees', type=int, default=1) parser.add_argument('--output_dir', type=str, default='dataset/') - parser.add_argument('--lpy_file', type=str, default='examples/Envy_tie_prune_label.lpy') + parser.add_argument('--lpy_file', type=str, default='examples/UFO_tie_prune_label.lpy') parser.add_argument('--verbose', action='store_true', default=False) args = parser.parse_args() num_trees = args.num_trees @@ -30,6 +30,7 @@ l.plot(lstring) # l.plot() scene = l.sceneInterpretation(lstring) + # scene.save("{}/tree_{}.lpy".format(output_dir, i)) if not os.path.exists(output_dir): os.makedirs(output_dir) if args.verbose: From badf1dbc9f9dba82745d83124e82b250051796c6 Mon Sep 17 00:00:00 2001 From: Gemini Date: Mon, 10 Nov 2025 15:13:33 -0800 Subject: [PATCH 04/14] Intermediate refactor: --- examples/Camp_Envy_tie_prune_label.lpy | 18 +- examples/Envy_tie_prune_label.lpy | 10 +- examples/UFO/UFO.lpy | 201 ++++++++++++++++++++ examples/UFO/UFOconfigs.py | 186 ++++++++++++++++++ examples/UFO_tie_prune_label.lpy | 158 +++++++--------- helper.py | 125 ++++++++++++- stochastic_tree.py | 249 ++++++++++++++++--------- tree_generation/make_n_trees.py | 2 +- 8 files changed, 751 insertions(+), 198 deletions(-) create mode 100644 examples/UFO/UFO.lpy create mode 100644 examples/UFO/UFOconfigs.py diff --git a/examples/Camp_Envy_tie_prune_label.lpy b/examples/Camp_Envy_tie_prune_label.lpy index b260c36..2f3dc6f 100644 --- a/examples/Camp_Envy_tie_prune_label.lpy +++ b/examples/Camp_Envy_tie_prune_label.lpy @@ -83,7 +83,7 @@ class Spur(BasicWood): self.name = str(self.__class__.__name__) +'_'+ str(self.__class__.count) Spur.count+=1 self.num_leaves = 0 - self.contour = create_noisy_circle_curve(1, .2, 30) + self.contour = create_noisy_branch_contour(1, .2, 30) def is_bud_break(self, num_buds_segment): return (rd.random() < 0.1) @@ -119,7 +119,7 @@ class Branch(BasicWood): self.name = str(self.__class__.__name__) +'_'+ str(self.__class__.count) Branch.count+=1 self.num_buds_segment=0 - self.contour = create_noisy_circle_curve(1, .2, 30) + self.contour = create_noisy_branch_contour(1, .2, 30) def is_bud_break(self, num_buds_segment): return (rd.random() < 0.01*(1 - self.num_buds_segment/self.growth.max_buds_segment)) @@ -151,7 +151,7 @@ class Trunk(BasicWood): if not name: self.name = str(self.__class__.__name__) +'_'+ str(self.__class__.count) Trunk.count+=1 - self.contour = create_noisy_circle_curve(1, .2, 30) + self.contour = create_noisy_branch_contour(1, .2, 30) def is_bud_break(self, num_buds_segment): if (rd.random() > 0.2*(1 - num_buds_segment/self.growth.max_buds_segment)): @@ -183,7 +183,7 @@ class NonTrunk(BasicWood): if not name: self.name = str(self.__class__.__name__) +'_'+ str(self.__class__.count) Branch.count+=1 - self.contour = create_noisy_circle_curve(1, .2, 30) + self.contour = create_noisy_branch_contour(1, .2, 30) def is_bud_break(self, num_buds_segment): return (rd.random() < 0.02*(1 - num_buds_segment/self.growth.max_buds_segment)) @@ -335,7 +335,7 @@ rd.seed(seed_val) # Added by Camp -def create_noisy_circle_curve(radius, noise_factor, num_points=100, seed=None): +def create_noisy_branch_contour(radius, noise_factor, num_points=100, seed=None): if seed is not None: rd.seed(seed) t = np.linspace(0, 2 * np.pi, num_points, endpoint=False) @@ -463,13 +463,13 @@ def make_leaf_guide(): def reset_contour(): - default_curve = create_noisy_circle_curve(1, 0, 30) # Example of a simple default contour + default_curve = create_noisy_branch_contour(1, 0, 30) # Example of a simple default contour nproduce SetContour(default_curve) global profile1, profile2, profile3 -profile1 = create_noisy_circle_curve(1, .2, 30, 23) -profile2 = create_noisy_circle_curve(1, .2, 30) -profile3 = create_noisy_circle_curve(1, .2, 30) +profile1 = create_noisy_branch_contour(1, .2, 30, 23) +profile2 = create_noisy_branch_contour(1, .2, 30) +profile3 = create_noisy_branch_contour(1, .2, 30) # print("Labelling: ", label) # print("Seed ", seed_val) diff --git a/examples/Envy_tie_prune_label.lpy b/examples/Envy_tie_prune_label.lpy index 70fefb5..5975ed2 100644 --- a/examples/Envy_tie_prune_label.lpy +++ b/examples/Envy_tie_prune_label.lpy @@ -40,7 +40,7 @@ class Spur(BasicWood): # Every branch gets its own contour when it is constructed to ensure # branches each having a unique profile curve - self.contour = create_noisy_circle_curve(radius = 2, noise_factor = .2, num_points = 30) + self.contour = create_noisy_branch_contour(radius = 2, noise_factor = .2, num_points = 30) def is_bud_break(self, num_buds_segment): return (rd.random() < 0.1) @@ -72,7 +72,7 @@ class Branch(BasicWood): # Every branch gets its own contour when it is constructed to ensure # branches each having a unique profile curve - self.contour = create_noisy_circle_curve(1, .5, 30) + self.contour = create_noisy_branch_contour(1, .5, 30) def is_bud_break(self, num_break_buds): if num_break_buds >= 1: @@ -109,7 +109,7 @@ class Trunk(BasicWood): # Every branch gets its own contour when it is constructed to ensure # branches each having a unique profile curve - self.contour = create_noisy_circle_curve(3, 0.05, 30) + self.contour = create_noisy_branch_contour(3, 0.05, 30) def is_bud_break(self, num_buds_segment): if (rd.random() > 0.1*(1 - num_buds_segment/self.growth.max_buds_segment)): @@ -145,7 +145,7 @@ class NonTrunk(BasicWood): # Every branch gets its own contour when it is constructed to ensure # branches each having a unique profile curve - self.contour = create_noisy_circle_curve(1, .2, 30) + self.contour = create_noisy_branch_contour(1, .2, 30) def is_bud_break(self, num_buds_segment): return (rd.random() < 0.5*(1 - num_buds_segment/self.growth.max_buds_segment)) @@ -304,7 +304,7 @@ rd.seed(seed_val) # Used to set the contour back to a circle after it has been changed for a branch def reset_contour(): - default_curve = create_noisy_circle_curve(1, 0, 30) # Example of a simple default contour + default_curve = create_noisy_branch_contour(1, 0, 30) # Example of a simple default contour nproduce SetContour(default_curve) diff --git a/examples/UFO/UFO.lpy b/examples/UFO/UFO.lpy new file mode 100644 index 0000000..a340fda --- /dev/null +++ b/examples/UFO/UFO.lpy @@ -0,0 +1,201 @@ +""" +Tying, Pruning and lablelling UFO architecture trees +""" +import sys +sys.path.append('../../') +from stochastic_tree import Support, BasicWood, TreeBranch, BasicWoodConfig +import time +from helper import * +import numpy as np +import random as rd +from examples.UFO.UFOconfigs import basicwood_prototypes, Trunk, Branch, TertiaryBranch, Spur, UFOSimulationConfig +from dataclasses import dataclass + + +#init +trunk_base = Trunk(copy_from = basicwood_prototypes['trunk']) +time_count = 0 + +# Create simulation configuration +simulation_config = UFOSimulationConfig() + +def generate_points_ufo(): + """ + Generate 3D points for the UFO trellis wire structure. + + Creates a linear array of wire attachment points along the x-axis at a fixed + height (z) and depth (y). The points are spaced evenly within the configured + x-range and used to construct the trellis support structure. + + Returns: + list: List of (x, y, z) tuples representing wire attachment points, + where all points share the same y and z coordinates. + + Configuration parameters used: + - ufo_x_range: Tuple (min_x, max_x) defining the range of x coordinates + - ufo_x_spacing: Spacing between consecutive x coordinates + - ufo_z_value: Fixed z-coordinate (height) for all points + - ufo_y_value: Fixed y-coordinate (depth) for all points + """ + x = np.arange( + simulation_config.ufo_x_range[0], + simulation_config.ufo_x_range[1], + simulation_config.ufo_x_spacing + ).astype(float) + z = np.full((x.shape[0],), simulation_config.ufo_z_value).astype(float) + y = np.full((x.shape[0],), simulation_config.ufo_y_value).astype(float) + + pts = [] + for i in range(x.shape[0]): + pts.append((x[i], y[i], z[i])) + + return pts + + +# def is_eligible_for_tying(module): +# """ +# Check if a module represents an eligible branch segment for tying operations. + +# A module is eligible if it: +# - Is a 'WoodStart' symbol (completed branch segment) +# - Has tying functionality +# - Has a valid tie_axis (not None) + +# Args: +# module: L-System module to check + +# Returns: +# bool: True if module is eligible for tying +# """ +# print("Checking eligibility for tying:", module) +# return (module == 'WoodStart' and +# hasattr(module[0].type, 'tying') and +# getattr(module[0].type.tying, 'tie_axis', None) is not None) +support = Support(generate_points_ufo(), simulation_config.support_num_wires, simulation_config.support_spacing_wires, simulation_config.support_trunk_wire_point) +num_iteration_tie = simulation_config.num_iteration_tie +num_iteration_prune = simulation_config.num_iteration_prune +trunk_base.tying.guide_target = support.trunk_wire +###Tying stuff begins + + +def tie(lstring): + for j,i in enumerate(lstring): + # print("Evaluating module for tying:", i) + if i == 'WoodStart' and hasattr(i[0].type, 'tying') and getattr(i[0].type.tying, 'tie_axis', None) is not None: + if i[0].type.tying.tie_updated == False: + continue + curr = i[0] + if i[0].type.tying.guide_points: + i[0].type.tying.tie_updated = False + i[0].type.tying.guide_target.add_branch() + lstring, count = i[0].type.tie_lstring(lstring, j) + + return True + return False + + +def StartEach(lstring): + global parent_child_dict, support, trunk_base + if support.trunk_wire and trunk_base.tying.tie_updated == False: + trunk_base.tie_update() + + for i in parent_child_dict[trunk_base.name]: + if i.tying.tie_updated == False: + i.tie_update() + + +def EndEach(lstring): + global parent_child_dict, support, num_iteration_tie + + tied = False + if (getIterationNb()+1)%num_iteration_tie == 0: + + if support.trunk_wire : + trunk_base.update_guide(trunk_base.tying.guide_target) #Tie trunk one iteration before branches + energy_matrix = get_energy_mat(parent_child_dict[trunk_base.name], support, simulation_config) + decide_guide(energy_matrix, parent_child_dict[trunk_base.name], support, simulation_config) + for branch in parent_child_dict[trunk_base.name]: + branch.update_guide(branch.tying.guide_target) + while tie(lstring): + pass + if (getIterationNb() + 1) % num_iteration_prune == 0: + while pruning_strategy(lstring): # Prune branches until no more can be pruned + pass + return lstring + +def pruning_strategy(lstring): #Remove remnants of cut + cut = False + for j,i in enumerate(lstring): + if i.name == 'WoodStart' and i[0].type.info.age > simulation_config.pruning_age_threshold and i[0].type.tying.has_tied == False and i[0].type.info.cut == False: + i[0].type.info.cut = True + lstring = cut_from(j, lstring) + return True + return False + +parent_child_dict = {} +parent_child_dict[trunk_base.name] = [] +label = simulation_config.label +#Tie trunk +module Attractors +module grow_object +module bud +module WoodStart +curve = create_bezier_curve(x_range = (-1, 1), y_range = (-1, 1), z_range = (0, 10), seed_val=time.time()) +Axiom: Attractors(support)SetGuide(curve, trunk_base.growth.max_length)[@GcGetPos(trunk_base.location.start)T(ParameterSet(type = trunk_base))&(270)/(0)grow_object(trunk_base)GetPos(trunk_base.location.end)] +derivation length: simulation_config.derivation_length + +production: +#Decide whether branch internode vs trunk internode need to be the same size. +grow_object(o) : + if o == None: + produce * + if o.length >= o.growth.max_length: + nproduce * + else: + nproduce SetContour(o.contour) + o.grow_one() + if label: + r, g, b = o.info.color + nproduce SetColor(r,g,b) + if 'Spur' in o.name: + produce I(o.growth.growth_length, o.growth.thickness, o)bud(ParameterSet(type = o, num_buds = 0))@O(o.growth.thickness*simulation_config.thickness_multiplier)grow_object(o) + else: + nproduce I(o.growth.growth_length, o.growth.thickness, o) + if np.isclose(o.info.age % o.bud_spacing_age, 0, atol=simulation_config.bud_age_tolerance): + nproduce bud(ParameterSet(type = o, num_buds = 0)) + produce grow_object(o) + #produce I(o.growth_length, o.thickness, o)bud(ParameterSet(type = o, num_buds = 0))grow_object(o) + +bud(t) : + if t.type.is_bud_break(t.num_buds): + new_object = t.type.create_branch() + if new_object == None: + produce * + parent_child_dict[new_object.name] = [] + parent_child_dict[t.type.name].append(new_object) + #Store new object somewhere + t.num_buds+=1 + t.type.info.num_branches+=1 + + if hasattr(new_object, 'curve_x_range'): + import time + curve = create_bezier_curve(x_range=new_object.curve_x_range, y_range=new_object.curve_y_range, z_range=new_object.curve_z_range, seed_val=time.time()) + nproduce[@GcSetGuide(curve, new_object.growth.max_length) + else: + nproduce [ + nproduce @RGetPos(new_object.location.start)WoodStart(ParameterSet(type = new_object))/(rd.random()*360)&(rd.random()*360)grow_object(new_object)GetPos(new_object.location.end)@Ge]bud(t) + + +I(s,r,o) --> I(s,r+o.growth.thickness_increment, o) +_(r) --> _(r+o.growth.thickness_increment) + +homomorphism: + +I(a,r,o) --> F(a,r) +S(a,r,o) --> F(a,r) + +production: +Attractors(support): + pttodisplay = support.attractor_grid.get_enabled_points() + if len(pttodisplay) > 0: + produce [,(3) @g(PointSet(pttodisplay,width=simulation_config.attractor_point_width))] diff --git a/examples/UFO/UFOconfigs.py b/examples/UFO/UFOconfigs.py new file mode 100644 index 0000000..976b978 --- /dev/null +++ b/examples/UFO/UFOconfigs.py @@ -0,0 +1,186 @@ +import sys +sys.path.append('../../') +from stochastic_tree import Support, BasicWood, TreeBranch, BasicWoodConfig +import numpy as np +import random as rd +from dataclasses import dataclass +import copy +from helper import * +class Spur(TreeBranch): + def __init__(self, config=None, copy_from=None, prototype_dict: dict = {}): + super().__init__(config, copy_from, prototype_dict) + + def is_bud_break(self, num_buds_segment): + if num_buds_segment >= self.growth.max_buds_segment: + return False + return (rd.random() < 0.1) + + def create_branch(self): + return None + + +class TertiaryBranch(TreeBranch): + def __init__(self, config=None, copy_from=None, prototype_dict: dict = {}): + super().__init__(config, copy_from, prototype_dict) + + def is_bud_break(self, num_buds_segment): + if num_buds_segment >= 2: + return False + if (rd.random() < 0.005*self.growth.growth_length*(1 - self.num_buds/self.growth.max_buds_segment)): + self.num_buds +=1 + return True + + def create_branch(self): + if rd.random()>0.8: + new_ob = Branch(copy_from = self.prototype_dict['side_branch']) + else: + new_ob = Spur(copy_from = self.prototype_dict['spur']) + return new_ob + +class Branch(TreeBranch): + def __init__(self, config=None, copy_from=None, prototype_dict: dict = {}): + super().__init__(config, copy_from, prototype_dict) + + def is_bud_break(self, num_buds_segment): + if num_buds_segment >= 2: + return False + if (rd.random() < 0.2*(1 - self.num_buds/self.growth.max_buds_segment)): + + return True + + def create_branch(self): + try: + if rd.random()>0.9: + new_ob = TertiaryBranch(copy_from = self.prototype_dict['side_branch']) + else: + new_ob = Spur(copy_from = self.prototype_dict['spur']) + except: + return None + return new_ob + + +class Trunk(TreeBranch): + """ Details of the trunk while growing a tree, length, thickness, where to attach them etc """ + def __init__(self, config=None, copy_from=None, prototype_dict: dict = {}): + super().__init__(config, copy_from, prototype_dict) + + def is_bud_break(self, num_buds_segment): + if num_buds_segment >= self.growth.max_buds_segment: + return False + if (rd.random() > 0.05*self.length/self.growth.max_length*(1 - self.num_buds/self.growth.max_buds_segment)): + return False + self.num_buds+=1 + return True + + def create_branch(self): + if rd.random() > 0.1: + return Branch(copy_from = self.prototype_dict['branch']) + + + +# growth_length = 0.1 +basicwood_prototypes = {} + +# Create configs for cleaner prototype setup +spur_config = BasicWoodConfig( + max_buds_segment=5, + tie_axis=None, + max_length=0.1, + thickness=0.003, + growth_length=0.05, + thickness_increment=0., + color=[0, 255, 0], + bud_spacing_age=2, # Spurs bud every 1 age unit + curve_x_range=(-0.2, 0.2), # Tighter bounds for spur curves + curve_y_range=(-0.2, 0.2), # Tighter bounds for spur curves + curve_z_range=(-1, 1) # Same Z range +) + +side_branch_config = BasicWoodConfig( + max_buds_segment=40, + tie_axis=None, + max_length=0.25, + thickness=0.003, + growth_length=0.05, + thickness_increment=0.00001, + color=[0, 255, 0], + bud_spacing_age=2, # Tertiary branches bud every 3 age units + curve_x_range=(-0.5, 0.5), # Moderate bounds for tertiary branches + curve_y_range=(-0.5, 0.5), # Moderate bounds for tertiary branches + curve_z_range=(-1, 1) # Same Z range +) + +trunk_config = BasicWoodConfig( + max_buds_segment=60, + tie_axis=(1, 0, 0), + max_length=3, + thickness=0.02, + thickness_increment=0.00001, + growth_length=0.1, + color=[255, 0, 0], + bud_spacing_age=2, # Trunk buds every 4 age units + curve_x_range=(-0.3, 0.3), # Conservative bounds for trunk + curve_y_range=(-0.3, 0.3), # Conservative bounds for trunk + curve_z_range=(-0.5, 0.5) # Tighter Z range for trunk +) + +branch_config = BasicWoodConfig( + max_buds_segment=140, + tie_axis=(0, 0, 1), + max_length=2.5, + thickness=0.01, + thickness_increment=0.00001, + growth_length=0.1, + color=[255, 150, 0], + bud_spacing_age=2, # Branches bud every 2 age units + curve_x_range=(-0.4, 0.4), # Moderate bounds for primary branches + curve_y_range=(-0.4, 0.4), # Moderate bounds for primary branches + curve_z_range=(-1, 1) # Same Z range +) + +# Setup prototypes using configs +basicwood_prototypes['spur'] = Spur(config=spur_config, prototype_dict=basicwood_prototypes) +basicwood_prototypes['side_branch'] = TertiaryBranch(config=side_branch_config, prototype_dict=basicwood_prototypes) +basicwood_prototypes['trunk'] = Trunk(config=trunk_config, prototype_dict=basicwood_prototypes) +basicwood_prototypes['branch'] = Branch(config=branch_config, prototype_dict=basicwood_prototypes) + + + +@dataclass +class UFOSimulationConfig: + """Configuration for UFO trellis tree simulation parameters.""" + + # Tying and Pruning + num_iteration_tie: int = 8 + num_iteration_prune: int = 16 + + # Display + label: bool = True + + # Support Structure + support_trunk_wire_point: tuple = (0.6, 0, 0.4) + support_num_wires: int = 7 + support_spacing_wires: int = 1 + + # Point Generation + ufo_x_range: tuple = (0.65, 3) + ufo_x_spacing: float = 0.3 + ufo_z_value: float = 1.4 + ufo_y_value: float = 0 + + # Energy and Tying Parameters + energy_distance_weight: float = 0.5 # Weight for distance in energy calculation (was hardcoded /2) + energy_threshold: float = 1.0 # Maximum energy threshold for tying + + # Pruning Parameters + pruning_age_threshold: int = 8 # Age threshold for pruning untied branches + + # L-System Parameters + derivation_length: int = 160 # Number of derivation steps + + # Growth Parameters + thickness_multiplier: float = 1.2 # Multiplier for internode thickness + bud_age_tolerance: float = 0.01 # Tolerance for age-based bud spacing + + # Visualization Parameters + attractor_point_width: int = 10 # Width of attractor points in visualization \ No newline at end of file diff --git a/examples/UFO_tie_prune_label.lpy b/examples/UFO_tie_prune_label.lpy index 7a2063d..99206be 100644 --- a/examples/UFO_tie_prune_label.lpy +++ b/examples/UFO_tie_prune_label.lpy @@ -3,7 +3,7 @@ Tying, Pruning and lablelling UFO architecture trees """ import sys sys.path.append('../') -from stochastic_tree import Support, BasicWood +from stochastic_tree import Support, BasicWood, TreeBranch, BasicWoodConfig import numpy as np import random as rd import copy @@ -13,24 +13,12 @@ from helper import * # Used to set the contour back to a circle after it has been changed for a branch def reset_contour(): - default_curve = create_noisy_circle_curve(1, 0, 30) # Example of a simple default contour + default_curve = create_noisy_branch_contour(1, 0, 30) # Example of a simple default contour nproduce SetContour(default_curve) -class Spur(BasicWood): - count = 0 - def __init__(self, copy_from = None, max_buds_segment: int = 5, thickness: float = 0.1,\ - thickness_increment: float = 0.01, growth_length: float = 1., max_length: float = 7.,\ - tie_axis: tuple = (0,1,1), order: int = 1, prototype_dict: dict = {}, name = None, color = None): - super().__init__(copy_from, max_buds_segment,thickness, thickness_increment, growth_length,\ - max_length, tie_axis, order, color) - if copy_from: - self.__copy_constructor__(copy_from) - else: - self.prototype_dict = prototype_dict - if not name: - self.name = str(self.__class__.__name__) +'_'+ str(self.__class__.count) - Spur.count+=1 - self.contour = create_noisy_circle_curve(1, .2, 30) +class Spur(TreeBranch): + def __init__(self, config=None, copy_from=None, prototype_dict: dict = {}): + super().__init__(config, copy_from, prototype_dict) def is_bud_break(self, num_buds_segment): if num_buds_segment >= self.growth.max_buds_segment: @@ -39,29 +27,11 @@ class Spur(BasicWood): def create_branch(self): return None - - def grow(self): - pass -class LittleBranch(BasicWood): - count = 0 - def __init__(self, copy_from = None, max_buds_segment: int = 40, thickness: float = 0.1,\ - thickness_increment: float = 0.01, growth_length: float = 1., max_length: float = 7.,\ - tie_axis: tuple = (0,1,1), order: int = 1, prototype_dict: dict = {}, name = None, color = None): - - super().__init__(copy_from, max_buds_segment,thickness, thickness_increment, growth_length,\ - max_length, tie_axis, order, color) - if copy_from: - self.__copy_constructor__(copy_from) - else: - self.prototype_dict = prototype_dict - - if not name: - self.name = str(self.__class__.__name__) +'_'+ str(self.__class__.count) - LittleBranch.count+=1 - self.num_buds = 0 - self.contour = create_noisy_circle_curve(1, .2, 30) +class TertiaryBranch(TreeBranch): + def __init__(self, config=None, copy_from=None, prototype_dict: dict = {}): + super().__init__(config, copy_from, prototype_dict) def is_bud_break(self, num_buds_segment): if num_buds_segment >= 2: @@ -75,29 +45,11 @@ class LittleBranch(BasicWood): new_ob = Branch(copy_from = self.prototype_dict['side_branch']) else: new_ob = Spur(copy_from = self.prototype_dict['spur']) - return new_ob - - def grow(self): - pass - -class Branch(BasicWood): - count = 0 - def __init__(self, copy_from = None, max_buds_segment: int = 140, thickness: float = 0.1,\ - thickness_increment: float = 0.01, growth_length: float = 1., max_length: float = 7.,\ - tie_axis: tuple = (0,1,1), order: int = 1, prototype_dict: dict = {}, name = None, color = None): + return new_ob - super().__init__(copy_from, max_buds_segment,thickness, thickness_increment, growth_length,\ - max_length, tie_axis, order, color) - if copy_from: - self.__copy_constructor__(copy_from) - else: - self.prototype_dict = prototype_dict - - if not name: - self.name = str(self.__class__.__name__) +'_'+ str(self.__class__.count) - Branch.count+=1 - self.num_buds = 0 - self.contour = create_noisy_circle_curve(1, .2, 30) +class Branch(TreeBranch): + def __init__(self, config=None, copy_from=None, prototype_dict: dict = {}): + super().__init__(config, copy_from, prototype_dict) def is_bud_break(self, num_buds_segment): if num_buds_segment >= 2: @@ -109,35 +61,18 @@ class Branch(BasicWood): def create_branch(self): try: if rd.random()>0.9: - new_ob = LittleBranch(copy_from = self.prototype_dict['side_branch']) + new_ob = TertiaryBranch(copy_from = self.prototype_dict['side_branch']) else: new_ob = Spur(copy_from = self.prototype_dict['spur']) except: return None - return new_ob - - def grow(self): - pass + return new_ob -class Trunk(BasicWood): - count = 0 +class Trunk(TreeBranch): """ Details of the trunk while growing a tree, length, thickness, where to attach them etc """ - def __init__(self, copy_from = None, max_buds_segment: int = 60, thickness: float = 0.1,\ - thickness_increment: float = 0.01, growth_length: float = 1., max_length: float = 7.,\ - tie_axis: tuple = (0,1,1), order: int = 0, prototype_dict: dict = {}, name = None, color = None): - - super().__init__(copy_from, max_buds_segment,thickness, thickness_increment, growth_length,\ - max_length, tie_axis, order, color) - if copy_from: - self.__copy_constructor__(copy_from) - else: - self.prototype_dict = prototype_dict - if not name: - self.name = str(self.__class__.__name__) +'_'+ str(self.__class__.count) - Trunk.count+=1 - self.num_buds = 0 - self.contour = create_noisy_circle_curve(1, .2, 30) + def __init__(self, config=None, copy_from=None, prototype_dict: dict = {}): + super().__init__(config, copy_from, prototype_dict) def is_bud_break(self, num_buds_segment): if num_buds_segment >= self.growth.max_buds_segment: @@ -150,21 +85,58 @@ class Trunk(BasicWood): def create_branch(self): if rd.random() > 0.1: return Branch(copy_from = self.prototype_dict['branch']) - - def grow(self): - pass - -#Pass transition probabs? --> solve with abstract classes -growth_length = 0.1 +# growth_length = 0.1 basicwood_prototypes = {} -basicwood_prototypes['spur'] = Spur(tie_axis = (1, 0, 0), max_length = 0.1, thickness = 0.003, growth_length = growth_length/2, thickness_increment = 0., prototype_dict = basicwood_prototypes, color = [0, 255, 0]) -basicwood_prototypes['side_branch'] = LittleBranch(tie_axis = (0, 0, 1), max_length = 0.25, thickness = 0.003, growth_length =growth_length/2, thickness_increment = 0.00001, prototype_dict = basicwood_prototypes, color = [0, 255, 0]) -basicwood_prototypes['trunk'] = Trunk(tie_axis = (1, 0, 0), max_length = 3, thickness = 0.02, thickness_increment = 0.00001, growth_length = growth_length, prototype_dict = basicwood_prototypes, color = [255, 0, 0]) -basicwood_prototypes['branch'] = Branch(tie_axis = (0, 0, 1), max_length = 2.5, thickness = 0.01, thickness_increment = 0.00001, growth_length = growth_length, prototype_dict = basicwood_prototypes, color = [255, 150, 0]) +# Create configs for cleaner prototype setup +spur_config = BasicWoodConfig( + max_buds_segment=5, + tie_axis=(1, 0, 0), + max_length=0.1, + thickness=0.003, + growth_length=0.05, + thickness_increment=0., + color=[0, 255, 0] +) + +side_branch_config = BasicWoodConfig( + max_buds_segment=40, + tie_axis=(0, 0, 1), + max_length=0.25, + thickness=0.003, + growth_length=0.05, + thickness_increment=0.00001, + color=[0, 255, 0] +) + +trunk_config = BasicWoodConfig( + max_buds_segment=60, + tie_axis=(1, 0, 0), + max_length=3, + thickness=0.02, + thickness_increment=0.00001, + growth_length=0.1, + color=[255, 0, 0] +) + +branch_config = BasicWoodConfig( + max_buds_segment=140, + tie_axis=(0, 0, 1), + max_length=2.5, + thickness=0.01, + thickness_increment=0.00001, + growth_length=0.1, + color=[255, 150, 0] +) + +# Setup prototypes using configs +basicwood_prototypes['spur'] = Spur(config=spur_config, prototype_dict=basicwood_prototypes) +basicwood_prototypes['side_branch'] = TertiaryBranch(config=side_branch_config, prototype_dict=basicwood_prototypes) +basicwood_prototypes['trunk'] = Trunk(config=trunk_config, prototype_dict=basicwood_prototypes) +basicwood_prototypes['branch'] = Branch(config=branch_config, prototype_dict=basicwood_prototypes) #init trunk_base = Trunk(copy_from = basicwood_prototypes['trunk']) @@ -321,7 +293,7 @@ bud(t) : t.num_buds+=1 t.type.info.num_branches+=1 - if 'LittleBranch' in new_object.name: + if 'TertiaryBranch' in new_object.name: import time curve = create_bezier_curve(x_range=(-.5, .5), y_range=(-.5, .5), z_range=(-1, 1), seed_val=time.time()) nproduce[@GcSetGuide(curve, new_object.growth.max_length) diff --git a/helper.py b/helper.py index 454cfb3..7978ddb 100644 --- a/helper.py +++ b/helper.py @@ -2,7 +2,7 @@ from openalea.lpy import Lsystem, newmodule from random import uniform, seed from numpy import linspace, pi, sin, cos - +import numpy as np def amplitude(x): return 2 @@ -82,7 +82,7 @@ def gen_noise_branch(radius,nbp=20): pt/float(nbp-1),1) for pt in range(2,nbp)], degree=min(nbp-1,3),stride=nbp*100) -def create_noisy_circle_curve(radius, noise_factor, num_points=100, seed=None): +def create_noisy_branch_contour(radius, noise_factor, num_points=100, seed=None): if seed is not None: seed(seed) t = linspace(0, 2 * pi, num_points, endpoint=False) @@ -123,4 +123,123 @@ def create_bezier_curve(num_control_points=6, x_range=(-2,2), y_range=(-2, 2), z control_points_array = Point4Array(control_points) # Create and return the BezierCurve2D object bezier_curve = BezierCurve(control_points_array) - return bezier_curve \ No newline at end of file + return bezier_curve + + + +def get_energy_mat(branches, arch, simulation_config): + """ + Calculate the energy matrix for optimal branch-to-wire assignment. + + This function computes an energy cost matrix where each entry represents the + "cost" of assigning a specific branch to a specific wire in the trellis system. + The energy is based on the Euclidean distance from wire attachment points to + both the start and end points of each branch, weighted by the simulation's + distance weight parameter. + + The algorithm uses a greedy optimization approach where branches are assigned + to the lowest-energy available wire that hasn't reached capacity. + + Args: + branches: List of branch objects to be assigned to wires + arch: Support architecture object containing wire information + + Returns: + numpy.ndarray: Energy matrix of shape (num_branches, num_wires) where + matrix[i][j] is the energy cost of assigning branch i to wire j. + Untied branches and occupied wires have infinite energy (np.inf). + """ + num_branches = len(branches) + num_wires = len(arch.branch_supports) + + # Initialize energy matrix with infinite values (impossible assignments) + energy_matrix = np.full((num_branches, num_wires), np.inf) + + # Calculate energy costs for all valid branch-wire combinations + for branch_idx, branch in enumerate(branches): + # Skip branches that are already tied + if branch.tying.has_tied: + continue + + for wire_id, wire in arch.branch_supports.items(): + # Skip wires that already have a branch attached + if wire.num_branch >= 1: + continue + + # Calculate weighted distance energy for this branch-wire pair + # Energy considers distance from wire to both branch endpoints + wire_point = np.array(wire.point) + branch_start = np.array(branch.location.start) + branch_end = np.array(branch.location.end) + + start_distance_energy = np.sum((wire_point - branch_start) ** 2) + end_distance_energy = np.sum((wire_point - branch_end) ** 2) + + total_energy = (start_distance_energy + end_distance_energy) * simulation_config.energy_distance_weight + + energy_matrix[branch_idx, wire_id] = total_energy + + return energy_matrix + +def decide_guide(energy_matrix, branches, arch, simulation_config): + """ + Perform greedy assignment of branches to wires based on energy matrix. + + This function implements a greedy optimization algorithm that iteratively assigns + the branch-wire pair with the lowest energy cost. Once a branch is assigned to + a wire, both that branch and wire are marked as unavailable (infinite energy) + to prevent further assignments. + + The algorithm continues until no valid assignments remain (all remaining energies + are infinite or above the threshold). + + Args: + energy_matrix: numpy.ndarray of shape (num_branches, num_wires) with energy costs + branches: List of branch objects to be assigned + arch: Support architecture containing wire information + + Returns: + None: Modifies branches and arch in-place with new assignments + """ + num_branches, num_wires = energy_matrix.shape + + # Early return if no branches or wires to assign + if num_branches == 0 or num_wires == 0: + return + + # Continue making assignments until no valid ones remain + while True: + # Find the minimum energy value and its position + min_energy_indices = np.argwhere(energy_matrix == np.min(energy_matrix)) + + # If no valid indices found or matrix is empty, stop + if len(min_energy_indices) == 0: + break + + # Get the first (and typically only) minimum energy position + branch_idx, wire_id = min_energy_indices[0] + min_energy = energy_matrix[branch_idx, wire_id] + + # Stop if minimum energy is infinite (no valid assignments) or above threshold + if np.isinf(min_energy) or min_energy > simulation_config.energy_threshold: + break + + # Get the branch and wire objects + branch = branches[branch_idx] + wire = arch.branch_supports[wire_id] + + # Skip if branch is already tied (defensive check) + if branch.tying.has_tied: + # Mark this assignment as invalid and continue + energy_matrix[branch_idx, wire_id] = np.inf + continue + + # Perform the assignment + branch.tying.guide_target = wire + wire.add_branch() + + # Mark branch and wire as unavailable for future assignments + # Set entire row (branch) to infinity - this branch can't be assigned again + energy_matrix[branch_idx, :] = np.inf + # Set entire column (wire) to infinity - this wire can't accept more branches + energy_matrix[:, wire_id] = np.inf \ No newline at end of file diff --git a/stochastic_tree.py b/stochastic_tree.py index 8dbda36..e526e25 100644 --- a/stochastic_tree.py +++ b/stochastic_tree.py @@ -2,35 +2,101 @@ Defines the abstract class BasicWood, class Wire and class Support. """ -from abc import abstractmethod +from abc import ABC, abstractmethod from openalea.plantgl.all import * import copy import numpy as np from openalea.plantgl.scenegraph.cspline import CSpline import random as rd - +from helper import create_noisy_branch_contour import collections +from dataclasses import dataclass +from typing import Tuple + eps = 1e-6 -from abc import ABC, abstractmethod -# class Tree(): -# #branch_dict = {} -# trunk_dict = {} -# """ This class will have all the parameters required to grow the tree, i.e. the transition -# prob, max trunk length, max branch length etc. Each tree will have its own children branch and trunk classes """ -# def __init__(self): -# self.trunk_num_buds_segment = 5 -# self.branch_num_buds_segment = 5 -# self.trunk_bud_break_prob = 0.5 -# self.branch_bud_break_prob = 0.5 -# self.num_branches = 0 -# self.num_trunks = 0 - - -# # BRANCH AND TRUNK SUBCLASS OF WOOD +@dataclass +class LocationState: + """Location tracking for a wood object: start point, end point, and last tie location.""" + start: any = None # Vector3 + end: any = None # Vector3 + last_tie_location: any = None # Vector3 + + def __post_init__(self): + """Initialize Vector3 points if not provided.""" + if self.start is None: + self.start = Vector3(0, 0, 0) + if self.end is None: + self.end = Vector3(0, 0, 0) + if self.last_tie_location is None: + self.last_tie_location = Vector3(0, 0, 0) -class BasicWood(ABC): +@dataclass +class TyingState: + """Tying and guiding state for a wood object.""" + has_tied: bool = False + guide_points: list = None # List of (x,y,z) tuples for spline control points + guide_target: any = -1 # Wire object or -1 (no target) + tie_axis: tuple = (0, 1, 1) # Direction vector for the wire axis + tie_updated: bool = False + + def __post_init__(self): + """Initialize guide_points as empty list if not provided.""" + if self.guide_points is None: + self.guide_points = [] + +@dataclass +class GrowthState: + """Growth parameters for a wood object.""" + max_buds_segment: int = 5 + thickness: float = 0.1 + thickness_increment: float = 0.01 + growth_length: float = 1.0 + max_length: float = 7.0 +@dataclass +class InfoState: + """Information/metadata for a wood object.""" + age: int = 0 + cut: bool = False + prunable: bool = True + order: int = 0 + num_branches: int = 0 + color: int = 0 + material: int = 0 + branch_dict: any = None # collections.deque + + def __post_init__(self): + """Initialize branch_dict if not provided.""" + if self.branch_dict is None: + self.branch_dict = collections.deque() + +@dataclass +class BasicWoodConfig: + """Configuration parameters for BasicWood initialization.""" + copy_from: any = None + max_buds_segment: int = 5 + thickness: float = 0.1 + thickness_increment: float = 0.01 + growth_length: float = 1.0 + max_length: float = 7.0 + tie_axis: tuple = None + order: int = 0 + color: int = 0 + material: int = 0 + name: str = None + bud_spacing_age: int = 2 # Age interval for bud creation + + # Curve parameters for L-System growth guides + curve_x_range: tuple = (-0.5, 0.5) # X bounds for Bezier curve control points + curve_y_range: tuple = (-0.5, 0.5) # Y bounds for Bezier curve control points + curve_z_range: tuple = (-1, 1) # Z bounds for Bezier curve control points + + def __post_init__(self): + """Initialize mutable defaults if needed.""" + pass + +class BasicWood(ABC): @staticmethod def clone(obj): try: @@ -38,11 +104,33 @@ def clone(obj): except copy.Error: raise copy.Error(f'Not able to copy {obj}') from None - def __init__(self, copy_from = None, max_buds_segment: int = 5, thickness: float = 0.1,\ - thickness_increment: float = 0.01, growth_length: float = 1., max_length: float = 7.,\ - tie_axis: list = (0,1,1), order: int = 0, color: int = 0, material = 0, name: str = None):#,\ - #bud_break_prob_func: "Function" = lambda x,y: rd.random()): - + def __init__(self, config=None, copy_from=None, **kwargs): + + # Validate parameters + if copy_from is None and config is None: + raise ValueError("Either 'config' or 'copy_from' must be provided") + + # Handle config-based initialization + if config is not None and isinstance(config, BasicWoodConfig): + # Use config values + copy_from = config.copy_from if copy_from is None else copy_from + max_buds_segment = config.max_buds_segment + thickness = config.thickness + thickness_increment = config.thickness_increment + growth_length = config.growth_length + max_length = config.max_length + tie_axis = config.tie_axis + order = config.order + color = config.color + material = config.material + name = config.name + bud_spacing_age = config.bud_spacing_age + curve_x_range = config.curve_x_range + curve_y_range = config.curve_y_range + curve_z_range = config.curve_z_range + elif copy_from is None: + raise ValueError("config must be provided when copy_from is None") + #Location variables if copy_from: self.__copy_constructor__(copy_from) @@ -62,6 +150,13 @@ def __init__(self, copy_from = None, max_buds_segment: int = 5, thickness: float growth_length=growth_length, max_length=max_length ) + # Bud spacing for L-System rules + self.bud_spacing_age = bud_spacing_age + + # Curve parameters for L-System growth guides + self.curve_x_range = curve_x_range + self.curve_y_range = curve_y_range + self.curve_z_range = curve_z_range @@ -70,15 +165,7 @@ def __copy_constructor__(self, copy_from): for k,v in update_dict.items(): setattr(self, k, v) #self.__dict__.update(update_dict) - - # ===== Location property accessors for backward compatibility ===== - - # ===== Tying property accessors for backward compatibility ===== - - # ===== Growth property accessors for backward compatibility ===== - - # ===== Info property accessors for backward compatibility ===== - + @abstractmethod def is_bud_break(self) -> bool: """This method defines if a bud will break or not -> returns true for yes, false for not. Input can be any variables""" @@ -291,64 +378,52 @@ def _get_parallel_and_perpendicular_components(self, vec_a, vec_b): return parallel_component, perpendicular_component -from dataclasses import dataclass -from typing import Tuple -@dataclass -class LocationState: - """Location tracking for a wood object: start point, end point, and last tie location.""" - start: any = None # Vector3 - end: any = None # Vector3 - last_tie_location: any = None # Vector3 +class TreeBranch(BasicWood): + """Base class for all tree branch types with common initialization logic""" - def __post_init__(self): - """Initialize Vector3 points if not provided.""" - if self.start is None: - self.start = Vector3(0, 0, 0) - if self.end is None: - self.end = Vector3(0, 0, 0) - if self.last_tie_location is None: - self.last_tie_location = Vector3(0, 0, 0) - -@dataclass -class TyingState: - """Tying and guiding state for a wood object.""" - has_tied: bool = False - guide_points: list = None # List of (x,y,z) tuples for spline control points - guide_target: any = -1 # Wire object or -1 (no target) - tie_axis: tuple = (0, 1, 1) # Direction vector for the wire axis - tie_updated: bool = False + count = 0 # Class variable for instance counting - def __post_init__(self): - """Initialize guide_points as empty list if not provided.""" - if self.guide_points is None: - self.guide_points = [] - -@dataclass -class GrowthState: - """Growth parameters for a wood object.""" - max_buds_segment: int = 5 - thickness: float = 0.1 - thickness_increment: float = 0.01 - growth_length: float = 1.0 - max_length: float = 7.0 - -@dataclass -class InfoState: - """Information/metadata for a wood object.""" - age: int = 0 - cut: bool = False - prunable: bool = True - order: int = 0 - num_branches: int = 0 - color: int = 0 - material: int = 0 - branch_dict: any = None # collections.deque + def __init__(self, config=None, copy_from=None, prototype_dict: dict = {}, + name: str = None, contour_params: tuple = (1, 0.2, 30)): + + # Validate parameters + if copy_from is None and config is None: + raise ValueError("Either 'config' or 'copy_from' must be provided") + + # Call BasicWood constructor + super().__init__(config, copy_from) + + # Handle copy construction vs new instance + if copy_from: + # BasicWood already handled the copy, just set additional attributes + pass + else: + self.prototype_dict = prototype_dict + + # Set name with automatic numbering + if not name: + self.name = f"{self.__class__.__name__}_{self.__class__.count}" + self.__class__.count += 1 + + # Set up contour (subclasses can override contour_params) + radius, noise_factor, num_points = contour_params + self.contour = create_noisy_branch_contour(radius, noise_factor, num_points) + + # Initialize common attributes + self.num_buds = 0 + + # Initialize subclass-specific attributes + self._init_subclass_attributes() + + def _init_subclass_attributes(self): + """Hook for subclasses to initialize their specific attributes""" + pass + + def grow(self): + """Default empty implementation - subclasses can override if needed""" + pass - def __post_init__(self): - """Initialize branch_dict if not provided.""" - if self.branch_dict is None: - self.branch_dict = collections.deque() @dataclass class Wire: diff --git a/tree_generation/make_n_trees.py b/tree_generation/make_n_trees.py index ee18e65..0cc9e37 100644 --- a/tree_generation/make_n_trees.py +++ b/tree_generation/make_n_trees.py @@ -11,7 +11,7 @@ parser = argparse.ArgumentParser() parser.add_argument('--num_trees', type=int, default=1) parser.add_argument('--output_dir', type=str, default='dataset/') - parser.add_argument('--lpy_file', type=str, default='examples/UFO_tie_prune_label.lpy') + parser.add_argument('--lpy_file', type=str, default='examples/UFO/UFO.lpy') parser.add_argument('--verbose', action='store_true', default=False) args = parser.parse_args() num_trees = args.num_trees From 558c8859096d1d0923c268e7bbd3c2d696b4b419 Mon Sep 17 00:00:00 2001 From: Gemini Date: Mon, 10 Nov 2025 15:22:12 -0800 Subject: [PATCH 05/14] simpler tie conditions --- examples/UFO/UFO.lpy | 26 ++++---------------------- stochastic_tree.py | 2 +- 2 files changed, 5 insertions(+), 23 deletions(-) diff --git a/examples/UFO/UFO.lpy b/examples/UFO/UFO.lpy index a340fda..eb00d3c 100644 --- a/examples/UFO/UFO.lpy +++ b/examples/UFO/UFO.lpy @@ -52,25 +52,7 @@ def generate_points_ufo(): return pts -# def is_eligible_for_tying(module): -# """ -# Check if a module represents an eligible branch segment for tying operations. - -# A module is eligible if it: -# - Is a 'WoodStart' symbol (completed branch segment) -# - Has tying functionality -# - Has a valid tie_axis (not None) - -# Args: -# module: L-System module to check - -# Returns: -# bool: True if module is eligible for tying -# """ -# print("Checking eligibility for tying:", module) -# return (module == 'WoodStart' and -# hasattr(module[0].type, 'tying') and -# getattr(module[0].type.tying, 'tie_axis', None) is not None) + support = Support(generate_points_ufo(), simulation_config.support_num_wires, simulation_config.support_spacing_wires, simulation_config.support_trunk_wire_point) num_iteration_tie = simulation_config.num_iteration_tie num_iteration_prune = simulation_config.num_iteration_prune @@ -80,8 +62,7 @@ trunk_base.tying.guide_target = support.trunk_wire def tie(lstring): for j,i in enumerate(lstring): - # print("Evaluating module for tying:", i) - if i == 'WoodStart' and hasattr(i[0].type, 'tying') and getattr(i[0].type.tying, 'tie_axis', None) is not None: + if (i == 'WoodStart' and hasattr(i[0].type, 'tying')) and getattr(i[0].type.tying, 'tie_axis', None) is not None: if i[0].type.tying.tie_updated == False: continue curr = i[0] @@ -139,9 +120,10 @@ label = simulation_config.label module Attractors module grow_object module bud +module branch module WoodStart curve = create_bezier_curve(x_range = (-1, 1), y_range = (-1, 1), z_range = (0, 10), seed_val=time.time()) -Axiom: Attractors(support)SetGuide(curve, trunk_base.growth.max_length)[@GcGetPos(trunk_base.location.start)T(ParameterSet(type = trunk_base))&(270)/(0)grow_object(trunk_base)GetPos(trunk_base.location.end)] +Axiom: Attractors(support)SetGuide(curve, trunk_base.growth.max_length)[@GcGetPos(trunk_base.location.start)WoodStart(ParameterSet(type = trunk_base))&(270)/(0)grow_object(trunk_base)GetPos(trunk_base.location.end)] derivation length: simulation_config.derivation_length production: diff --git a/stochastic_tree.py b/stochastic_tree.py index e526e25..204e968 100644 --- a/stochastic_tree.py +++ b/stochastic_tree.py @@ -37,7 +37,7 @@ class TyingState: has_tied: bool = False guide_points: list = None # List of (x,y,z) tuples for spline control points guide_target: any = -1 # Wire object or -1 (no target) - tie_axis: tuple = (0, 1, 1) # Direction vector for the wire axis + tie_axis: tuple = None # Direction vector for the wire axis tie_updated: bool = False def __post_init__(self): From 3cd69d1aa21fe8a72f741165d9bc77b154d7d392 Mon Sep 17 00:00:00 2001 From: Gemini Date: Mon, 10 Nov 2025 15:44:30 -0800 Subject: [PATCH 06/14] Finished refactoring UFO.lpy --- examples/UFO/UFO.lpy | 341 ++++++++++++++++++++++--------------- examples/UFO/UFOconfigs.py | 36 +++- helper.py | 109 +++++++++++- 3 files changed, 350 insertions(+), 136 deletions(-) diff --git a/examples/UFO/UFO.lpy b/examples/UFO/UFO.lpy index eb00d3c..4f2e000 100644 --- a/examples/UFO/UFO.lpy +++ b/examples/UFO/UFO.lpy @@ -8,176 +8,249 @@ import time from helper import * import numpy as np import random as rd -from examples.UFO.UFOconfigs import basicwood_prototypes, Trunk, Branch, TertiaryBranch, Spur, UFOSimulationConfig +from examples.UFO.UFOconfigs import basicwood_prototypes, Trunk, Branch, TertiaryBranch, Spur, UFOSimulationConfig, generate_points_ufo from dataclasses import dataclass #init -trunk_base = Trunk(copy_from = basicwood_prototypes['trunk']) -time_count = 0 +main_trunk = Trunk(copy_from = basicwood_prototypes['trunk']) # Create simulation configuration simulation_config = UFOSimulationConfig() -def generate_points_ufo(): + + +trellis_support = Support(generate_points_ufo(), simulation_config.support_num_wires, simulation_config.support_spacing_wires, simulation_config.support_trunk_wire_point) +tying_interval_iterations = simulation_config.num_iteration_tie +pruning_interval_iterations = simulation_config.num_iteration_prune +main_trunk.tying.guide_target = trellis_support.trunk_wire + +def StartEach(lstring): """ - Generate 3D points for the UFO trellis wire structure. + Initialize tying updates for trunk and branches at the start of each iteration. + + This function is called at the beginning of each L-System derivation iteration + to prepare the tree structure for potential tying operations. It ensures that + both the trunk and all child branches are ready to participate in the tying + process by updating their tying status. - Creates a linear array of wire attachment points along the x-axis at a fixed - height (z) and depth (y). The points are spaced evenly within the configured - x-range and used to construct the trellis support structure. + The function performs two main tasks: + 1. Updates the trunk's tying status if it has a wire target and hasn't been updated + 2. Updates all child branches' tying status if they haven't been updated + Args: + lstring: The current L-System string (not used in this function but required + by L-Py's callback interface) + Returns: - list: List of (x, y, z) tuples representing wire attachment points, - where all points share the same y and z coordinates. - - Configuration parameters used: - - ufo_x_range: Tuple (min_x, max_x) defining the range of x coordinates - - ufo_x_spacing: Spacing between consecutive x coordinates - - ufo_z_value: Fixed z-coordinate (height) for all points - - ufo_y_value: Fixed y-coordinate (depth) for all points + None: Modifies global tree structures in-place + + Note: + This function relies on global variables: branch_hierarchy, trellis_support, main_trunk. + It should be called automatically by L-Py at the start of each iteration. """ - x = np.arange( - simulation_config.ufo_x_range[0], - simulation_config.ufo_x_range[1], - simulation_config.ufo_x_spacing - ).astype(float) - z = np.full((x.shape[0],), simulation_config.ufo_z_value).astype(float) - y = np.full((x.shape[0],), simulation_config.ufo_y_value).astype(float) - - pts = [] - for i in range(x.shape[0]): - pts.append((x[i], y[i], z[i])) + global branch_hierarchy, trellis_support, main_trunk - return pts + # Update trunk tying status if it has a wire target and needs updating + if trellis_support.trunk_wire and not main_trunk.tying.tie_updated: + main_trunk.tie_update() + # Update tying status for all child branches that need it + for branch in branch_hierarchy[main_trunk.name]: + if not branch.tying.tie_updated: + branch.tie_update() + +def EndEach(lstring): + """ + Perform tying and pruning operations at the end of each L-System iteration. -support = Support(generate_points_ufo(), simulation_config.support_num_wires, simulation_config.support_spacing_wires, simulation_config.support_trunk_wire_point) -num_iteration_tie = simulation_config.num_iteration_tie -num_iteration_prune = simulation_config.num_iteration_prune -trunk_base.tying.guide_target = support.trunk_wire -###Tying stuff begins - + This function is the main orchestration point for the tree training simulation. + It executes tying operations (assigning branches to trellis wires) and pruning + operations based on configurable iteration intervals. The tying process uses + energy-based optimization to find optimal branch-to-wire assignments. -def tie(lstring): - for j,i in enumerate(lstring): - if (i == 'WoodStart' and hasattr(i[0].type, 'tying')) and getattr(i[0].type.tying, 'tie_axis', None) is not None: - if i[0].type.tying.tie_updated == False: - continue - curr = i[0] - if i[0].type.tying.guide_points: - i[0].type.tying.tie_updated = False - i[0].type.tying.guide_target.add_branch() - lstring, count = i[0].type.tie_lstring(lstring, j) - - return True - return False - + The function performs the following sequence when tying is scheduled: + 1. Updates the trunk's guide target (ties trunk first) + 2. Calculates energy matrix for all branch-wire combinations + 3. Uses greedy optimization to assign branches to lowest-energy wires + 4. Updates guide targets for all assigned branches + 5. Performs actual tying operations in the L-System string + 6. Prunes old branches if pruning iteration is reached -def StartEach(lstring): - global parent_child_dict, support, trunk_base - if support.trunk_wire and trunk_base.tying.tie_updated == False: - trunk_base.tie_update() + Args: + lstring: The current L-System string containing modules and their parameters - for i in parent_child_dict[trunk_base.name]: - if i.tying.tie_updated == False: - i.tie_update() - - -def EndEach(lstring): - global parent_child_dict, support, num_iteration_tie - - tied = False - if (getIterationNb()+1)%num_iteration_tie == 0: - - if support.trunk_wire : - trunk_base.update_guide(trunk_base.tying.guide_target) #Tie trunk one iteration before branches - energy_matrix = get_energy_mat(parent_child_dict[trunk_base.name], support, simulation_config) - decide_guide(energy_matrix, parent_child_dict[trunk_base.name], support, simulation_config) - for branch in parent_child_dict[trunk_base.name]: - branch.update_guide(branch.tying.guide_target) - while tie(lstring): - pass - if (getIterationNb() + 1) % num_iteration_prune == 0: - while pruning_strategy(lstring): # Prune branches until no more can be pruned - pass - return lstring - -def pruning_strategy(lstring): #Remove remnants of cut - cut = False - for j,i in enumerate(lstring): - if i.name == 'WoodStart' and i[0].type.info.age > simulation_config.pruning_age_threshold and i[0].type.tying.has_tied == False and i[0].type.info.cut == False: - i[0].type.info.cut = True - lstring = cut_from(j, lstring) - return True - return False - -parent_child_dict = {} -parent_child_dict[trunk_base.name] = [] -label = simulation_config.label -#Tie trunk -module Attractors -module grow_object -module bud -module branch -module WoodStart -curve = create_bezier_curve(x_range = (-1, 1), y_range = (-1, 1), z_range = (0, 10), seed_val=time.time()) -Axiom: Attractors(support)SetGuide(curve, trunk_base.growth.max_length)[@GcGetPos(trunk_base.location.start)WoodStart(ParameterSet(type = trunk_base))&(270)/(0)grow_object(trunk_base)GetPos(trunk_base.location.end)] + Returns: + The modified L-System string after tying and pruning operations + + Note: + This function relies on global variables and is called automatically by L-Py + at the end of each derivation iteration. Tying occurs every tying_interval_iterations + iterations, while pruning occurs every pruning_interval_iterations iterations. + """ + global branch_hierarchy, trellis_support, tying_interval_iterations + + current_iteration = getIterationNb() + 1 #GetIterationNb is an L-Py function + + # Check if this is a tying iteration + if current_iteration % tying_interval_iterations == 0: + # Tie trunk to its target wire first (one iteration before branches) + if trellis_support.trunk_wire: + main_trunk.update_guide(main_trunk.tying.guide_target) + + # Calculate energy costs for optimal branch-to-wire assignments + branches = branch_hierarchy[main_trunk.name] + energy_matrix = get_energy_mat(branches, trellis_support, simulation_config) + + # Perform greedy optimization to assign branches to wires + decide_guide(energy_matrix, branches, trellis_support, simulation_config) + + # Update guide targets for all assigned branches + for branch in branches: + branch.update_guide(branch.tying.guide_target) + + # Execute tying operations in the L-System string + while tie(lstring, simulation_config): + pass # Continue until no more tying operations are possible + + # Check if this is also a pruning iteration + if current_iteration % pruning_interval_iterations == 0: + # Prune branches until no more can be pruned + while prune(lstring, simulation_config): + pass + + return lstring + + + +branch_hierarchy = {} +branch_hierarchy[main_trunk.name] = [] +enable_color_labeling = simulation_config.label +# ============================================================================= +# L-SYSTEM GRAMMAR DEFINITION +# ============================================================================= +# This section defines the formal grammar for the UFO tree growth simulation. +# The L-System uses modules (symbols) to represent different plant components +# and their growth behaviors. + +# ----------------------------------------------------------------------------- +# MODULE DECLARATIONS +# ----------------------------------------------------------------------------- +# Define the vocabulary of symbols used in the L-System grammar. +# Each module represents a different type of plant component or operation. + +module Attractors # Trellis support structure that guides branch growth +module grow_object # Growing plant parts (trunk, branches, spurs) with length/thickness +module bud # Dormant buds that can break to produce new branches +module branch # Branch segments in the L-System string +module WoodStart # Starting point of wood segments (used for tying operations) + +# ----------------------------------------------------------------------------- +# GLOBAL L-SYSTEM PARAMETERS +# ----------------------------------------------------------------------------- +# Create a growth guide curve for the initial trunk development +trunk_guide_curve = create_bezier_curve(x_range = (-1, 1), y_range = (-1, 1), z_range = (0, 10), seed_val=time.time()) + +# ----------------------------------------------------------------------------- +# AXIOM (STARTING STRING) +# ----------------------------------------------------------------------------- +# The initial L-System string that begins the simulation. +# Starts with the trellis attractors, sets up the trunk guide curve, +# and initializes the trunk growth with proper orientation. +Axiom: Attractors(trellis_support)SetGuide(trunk_guide_curve, main_trunk.growth.max_length)[@GcGetPos(main_trunk.location.start)WoodStart(ParameterSet(type = main_trunk))&(270)/(0)grow_object(main_trunk)GetPos(main_trunk.location.end)] + +# ----------------------------------------------------------------------------- +# DERIVATION PARAMETERS +# ----------------------------------------------------------------------------- +# Set the maximum number of derivation steps for the L-System derivation length: simulation_config.derivation_length +# ----------------------------------------------------------------------------- +# PRODUCTION RULES +# ----------------------------------------------------------------------------- +# Define how each module type evolves during each derivation step. +# These rules control the growth, branching, and development of the tree. + production: -#Decide whether branch internode vs trunk internode need to be the same size. -grow_object(o) : - if o == None: + +# GROW_OBJECT PRODUCTION RULE +# Handles the growth of plant segments (trunk, branches, spurs) +# Determines whether to continue growing, stop, or produce buds +grow_object(plant_segment) : + if plant_segment == None: + # Null object - terminate this branch produce * - if o.length >= o.growth.max_length: + if plant_segment.length >= plant_segment.growth.max_length: + # Maximum length reached - stop growing this segment nproduce * else: - nproduce SetContour(o.contour) - o.grow_one() - if label: - r, g, b = o.info.color + # Continue growing - update segment properties + nproduce SetContour(plant_segment.contour) + plant_segment.grow_one() + if enable_color_labeling: + # Add color visualization if labeling is enabled + r, g, b = plant_segment.info.color nproduce SetColor(r,g,b) - if 'Spur' in o.name: - produce I(o.growth.growth_length, o.growth.thickness, o)bud(ParameterSet(type = o, num_buds = 0))@O(o.growth.thickness*simulation_config.thickness_multiplier)grow_object(o) + if 'Spur' in plant_segment.name: + # Spur branches: produce short shoots with terminal buds + produce I(plant_segment.growth.growth_length, plant_segment.growth.thickness, plant_segment)bud(ParameterSet(type = plant_segment, num_buds = 0))@O(plant_segment.growth.thickness*simulation_config.thickness_multiplier)grow_object(plant_segment) else: - nproduce I(o.growth.growth_length, o.growth.thickness, o) - if np.isclose(o.info.age % o.bud_spacing_age, 0, atol=simulation_config.bud_age_tolerance): - nproduce bud(ParameterSet(type = o, num_buds = 0)) - produce grow_object(o) - #produce I(o.growth_length, o.thickness, o)bud(ParameterSet(type = o, num_buds = 0))grow_object(o) - -bud(t) : - if t.type.is_bud_break(t.num_buds): - new_object = t.type.create_branch() - if new_object == None: + # Regular branches: produce internode segment + nproduce I(plant_segment.growth.growth_length, plant_segment.growth.thickness, plant_segment) + if np.isclose(plant_segment.info.age % plant_segment.bud_spacing_age, 0, atol=simulation_config.bud_age_tolerance): + # Age-based bud production for lateral branching + nproduce bud(ParameterSet(type = plant_segment, num_buds = 0)) + produce grow_object(plant_segment) + +# BUD PRODUCTION RULE +# Controls bud break and branch initiation +# Determines when buds activate to produce new branches +bud(bud_parameters) : + if bud_parameters.type.is_bud_break(bud_parameters.num_buds): + # Bud break condition met - create new branch + new_branch = bud_parameters.type.create_branch() + if new_branch == None: + # Branch creation failed - terminate produce * - parent_child_dict[new_object.name] = [] - parent_child_dict[t.type.name].append(new_object) - #Store new object somewhere - t.num_buds+=1 - t.type.info.num_branches+=1 + # Register new branch in parent-child relationship tracking + branch_hierarchy[new_branch.name] = [] + branch_hierarchy[bud_parameters.type.name].append(new_branch) + # Update branch counters + bud_parameters.num_buds+=1 + bud_parameters.type.info.num_branches+=1 - if hasattr(new_object, 'curve_x_range'): + if hasattr(new_branch, 'curve_x_range'): + # Curved branches: set up custom growth guide curve import time - curve = create_bezier_curve(x_range=new_object.curve_x_range, y_range=new_object.curve_y_range, z_range=new_object.curve_z_range, seed_val=time.time()) - nproduce[@GcSetGuide(curve, new_object.growth.max_length) + curve = create_bezier_curve(x_range=new_branch.curve_x_range, y_range=new_branch.curve_y_range, z_range=new_branch.curve_z_range, seed_val=time.time()) + nproduce[@GcSetGuide(curve, new_branch.growth.max_length) else: + # Straight branches: use default orientation nproduce [ - nproduce @RGetPos(new_object.location.start)WoodStart(ParameterSet(type = new_object))/(rd.random()*360)&(rd.random()*360)grow_object(new_object)GetPos(new_object.location.end)@Ge]bud(t) - - -I(s,r,o) --> I(s,r+o.growth.thickness_increment, o) -_(r) --> _(r+o.growth.thickness_increment) + # Produce new branch with random orientation and growth object + nproduce @RGetPos(new_branch.location.start)WoodStart(ParameterSet(type = new_branch))/(rd.random()*360)&(rd.random()*360)grow_object(new_branch)GetPos(new_branch.location.end)@Ge]bud(bud_parameters) + +# ----------------------------------------------------------------------------- +# GEOMETRIC INTERPRETATION (HOMOMORPHISM) +# ----------------------------------------------------------------------------- +# Map abstract L-System modules to concrete 3D geometry for rendering. +# These rules define how the symbolic representation becomes visual. homomorphism: +# Internode segments become cylinders with length and radius I(a,r,o) --> F(a,r) +# Branch segments also become cylinders S(a,r,o) --> F(a,r) -production: -Attractors(support): - pttodisplay = support.attractor_grid.get_enabled_points() - if len(pttodisplay) > 0: - produce [,(3) @g(PointSet(pttodisplay,width=simulation_config.attractor_point_width))] +# ----------------------------------------------------------------------------- +# ATTRACTOR VISUALIZATION +# ----------------------------------------------------------------------------- +# Additional production rules for displaying trellis attractor points +production: +Attractors(trellis_support): + # Display enabled attractor points as visual markers + points_to_display = trellis_support.attractor_grid.get_enabled_points() + if len(points_to_display) > 0: + produce [,(3) @g(PointSet(points_to_display,width=simulation_config.attractor_point_width))] diff --git a/examples/UFO/UFOconfigs.py b/examples/UFO/UFOconfigs.py index 976b978..7133d51 100644 --- a/examples/UFO/UFOconfigs.py +++ b/examples/UFO/UFOconfigs.py @@ -183,4 +183,38 @@ class UFOSimulationConfig: bud_age_tolerance: float = 0.01 # Tolerance for age-based bud spacing # Visualization Parameters - attractor_point_width: int = 10 # Width of attractor points in visualization \ No newline at end of file + attractor_point_width: int = 10 # Width of attractor points in visualization + +def generate_points_ufo(): + """ + Generate 3D points for the UFO trellis wire structure. + + Creates a linear array of wire attachment points along the x-axis at a fixed + height (z) and depth (y). The points are spaced evenly within the configured + x-range and used to construct the trellis support structure. + + Returns: + list: List of (x, y, z) tuples representing wire attachment points, + where all points share the same y and z coordinates. + + Configuration parameters used: + - ufo_x_range: Tuple (min_x, max_x) defining the range of x coordinates + - ufo_x_spacing: Spacing between consecutive x coordinates + - ufo_z_value: Fixed z-coordinate (height) for all points + - ufo_y_value: Fixed y-coordinate (depth) for all points + """ + x = np.arange( + simulation_config.ufo_x_range[0], + simulation_config.ufo_x_range[1], + simulation_config.ufo_x_spacing + ).astype(float) + z = np.full((x.shape[0],), simulation_config.ufo_z_value).astype(float) + y = np.full((x.shape[0],), simulation_config.ufo_y_value).astype(float) + + wire_attachment_points = [] + for point_index in range(x.shape[0]): + wire_attachment_points.append((x[point_index], y[point_index], z[point_index])) + + return wire_attachment_points + + \ No newline at end of file diff --git a/helper.py b/helper.py index 7978ddb..83f0ec8 100644 --- a/helper.py +++ b/helper.py @@ -242,4 +242,111 @@ def decide_guide(energy_matrix, branches, arch, simulation_config): # Set entire row (branch) to infinity - this branch can't be assigned again energy_matrix[branch_idx, :] = np.inf # Set entire column (wire) to infinity - this wire can't accept more branches - energy_matrix[:, wire_id] = np.inf \ No newline at end of file + energy_matrix[:, wire_id] = np.inf + + +def prune(lstring, simulation_config): + """ + Prune old branches that exceed the age threshold and haven't been tied to wires. + + This function implements the pruning strategy for the tree training simulation. + It identifies branches that have grown too old (exceeding the pruning age threshold) + but haven't been successfully tied to trellis wires. Such branches are considered + unproductive and are removed from the L-System to encourage new growth. + + The pruning criteria are: + 1. Branch age exceeds the configured pruning threshold + 2. Branch has not been tied to any trellis wire + 3. Branch has not already been marked for cutting + + When a branch meets all criteria, it is: + - Marked as cut (to prevent re-processing) + - Removed from the L-System string using cut_from() + + Args: + lstring: The current L-System string containing modules and their parameters + + Returns: + bool: True if a branch was pruned, False if no eligible branches found + + Note: + This function processes one branch at a time and returns immediately after + pruning a single branch. It should be called repeatedly (e.g., in a while loop) + until no more pruning operations are possible. The cut_from() function handles + the actual removal of the branch and any dependent substructures from the string. + """ + for position, symbol in enumerate(lstring): + # Check if this is a WoodStart module (represents a branch) + if symbol.name == 'WoodStart': + branch = symbol[0].type + + # Check pruning criteria + age_exceeds_threshold = branch.info.age > simulation_config.pruning_age_threshold + not_tied_to_wire = not branch.tying.has_tied + not_already_cut = not branch.info.cut + + # Prune if all criteria are met + if age_exceeds_threshold and not_tied_to_wire and not_already_cut: + # Mark branch as cut to prevent re-processing + branch.info.cut = True + + # Remove the branch from the L-System string + lstring = cut_from(position, lstring) + + return True + + return False + + + +def tie(lstring, simulation_config): + """ + Perform tying operation on eligible branches in the L-System string. + + This function searches through the L-System string for 'WoodStart' modules that + represent branches ready for tying to trellis wires. It identifies branches that: + 1. Have tying properties (tying attribute exists) + 2. Have a defined tie axis (tie_axis is not None) + 3. Have not been tied yet (tie_updated is False) + 4. Have guide points available for wire attachment + + When an eligible branch is found, it performs the tying operation by: + - Marking the branch as tied (tie_updated = False) + - Adding the branch to the target wire + - Calling the branch's tie_lstring method to modify the L-System string + + Args: + lstring: The current L-System string containing modules and their parameters + + Returns: + bool: True if a tying operation was performed, False if no eligible branches found + + Note: + This function processes one branch at a time and returns immediately after + tying a single branch. It should be called repeatedly (e.g., in a while loop) + until no more tying operations are possible. + """ + for position, symbol in enumerate(lstring): + # Check if this is a WoodStart module with tying capabilities + if (symbol == 'WoodStart' and + hasattr(symbol[0].type, 'tying') and + getattr(symbol[0].type.tying, 'tie_axis', None) is not None): + + branch = symbol[0].type + + # Skip branches that have already been processed for tying + if not branch.tying.tie_updated: + continue + + # Check if branch has guide points for wire attachment + if branch.tying.guide_points: + # Perform the tying operation + branch.tying.tie_updated = False + branch.tying.guide_target.add_branch() + + # Update the L-System string with tying modifications + lstring, modifications_count = branch.tie_lstring(lstring, position) + + return True + + return False \ No newline at end of file From 9d65b5586dc7597734fb5c8b4bddd29a35d7afd5 Mon Sep 17 00:00:00 2001 From: Gemini Date: Mon, 10 Nov 2025 15:55:22 -0800 Subject: [PATCH 07/14] Refactored helper.py --- examples/UFO/UFO.lpy | 9 +- .../UFO/{UFOconfigs.py => UFO_prototypes.py} | 71 --- examples/UFO/UFO_simulation.py | 298 ++++++++++ .../Camp_Envy_tie_prune_label.lpy | 0 .../{ => legacy}/Envy_tie_prune_label.lpy | 0 examples/{ => legacy}/UFO_tie_prune_label.lpy | 0 examples/{ => legacy}/static_envy.lpy | 0 examples/{ => legacy}/static_ufo.lpy | 0 helper.py | 511 +++++++----------- 9 files changed, 500 insertions(+), 389 deletions(-) rename examples/UFO/{UFOconfigs.py => UFO_prototypes.py} (65%) create mode 100644 examples/UFO/UFO_simulation.py rename examples/{ => legacy}/Camp_Envy_tie_prune_label.lpy (100%) rename examples/{ => legacy}/Envy_tie_prune_label.lpy (100%) rename examples/{ => legacy}/UFO_tie_prune_label.lpy (100%) rename examples/{ => legacy}/static_envy.lpy (100%) rename examples/{ => legacy}/static_ufo.lpy (100%) diff --git a/examples/UFO/UFO.lpy b/examples/UFO/UFO.lpy index 4f2e000..e84e49d 100644 --- a/examples/UFO/UFO.lpy +++ b/examples/UFO/UFO.lpy @@ -8,7 +8,8 @@ import time from helper import * import numpy as np import random as rd -from examples.UFO.UFOconfigs import basicwood_prototypes, Trunk, Branch, TertiaryBranch, Spur, UFOSimulationConfig, generate_points_ufo +from examples.UFO.UFO_prototypes import basicwood_prototypes, Trunk +from examples.UFO.UFO_simulation import UFOSimulationConfig, generate_points_ufo, get_energy_mat, decide_guide, tie, prune from dataclasses import dataclass @@ -20,7 +21,7 @@ simulation_config = UFOSimulationConfig() -trellis_support = Support(generate_points_ufo(), simulation_config.support_num_wires, simulation_config.support_spacing_wires, simulation_config.support_trunk_wire_point) +trellis_support = Support(generate_points_ufo(simulation_config), simulation_config.support_num_wires, simulation_config.support_spacing_wires, simulation_config.support_trunk_wire_point) tying_interval_iterations = simulation_config.num_iteration_tie pruning_interval_iterations = simulation_config.num_iteration_prune main_trunk.tying.guide_target = trellis_support.trunk_wire @@ -150,7 +151,7 @@ module WoodStart # Starting point of wood segments (used for tying operation # GLOBAL L-SYSTEM PARAMETERS # ----------------------------------------------------------------------------- # Create a growth guide curve for the initial trunk development -trunk_guide_curve = create_bezier_curve(x_range = (-1, 1), y_range = (-1, 1), z_range = (0, 10), seed_val=time.time()) +trunk_guide_curve = create_bezier_curve(x_range = (-1, 1), y_range = (-1, 1), z_range = (0, 10), seed_value=time.time()) # ----------------------------------------------------------------------------- # AXIOM (STARTING STRING) @@ -223,7 +224,7 @@ bud(bud_parameters) : if hasattr(new_branch, 'curve_x_range'): # Curved branches: set up custom growth guide curve import time - curve = create_bezier_curve(x_range=new_branch.curve_x_range, y_range=new_branch.curve_y_range, z_range=new_branch.curve_z_range, seed_val=time.time()) + curve = create_bezier_curve(x_range=new_branch.curve_x_range, y_range=new_branch.curve_y_range, z_range=new_branch.curve_z_range, seed_value=time.time()) nproduce[@GcSetGuide(curve, new_branch.growth.max_length) else: # Straight branches: use default orientation diff --git a/examples/UFO/UFOconfigs.py b/examples/UFO/UFO_prototypes.py similarity index 65% rename from examples/UFO/UFOconfigs.py rename to examples/UFO/UFO_prototypes.py index 7133d51..0d67ffe 100644 --- a/examples/UFO/UFOconfigs.py +++ b/examples/UFO/UFO_prototypes.py @@ -146,75 +146,4 @@ def create_branch(self): -@dataclass -class UFOSimulationConfig: - """Configuration for UFO trellis tree simulation parameters.""" - - # Tying and Pruning - num_iteration_tie: int = 8 - num_iteration_prune: int = 16 - - # Display - label: bool = True - - # Support Structure - support_trunk_wire_point: tuple = (0.6, 0, 0.4) - support_num_wires: int = 7 - support_spacing_wires: int = 1 - - # Point Generation - ufo_x_range: tuple = (0.65, 3) - ufo_x_spacing: float = 0.3 - ufo_z_value: float = 1.4 - ufo_y_value: float = 0 - - # Energy and Tying Parameters - energy_distance_weight: float = 0.5 # Weight for distance in energy calculation (was hardcoded /2) - energy_threshold: float = 1.0 # Maximum energy threshold for tying - - # Pruning Parameters - pruning_age_threshold: int = 8 # Age threshold for pruning untied branches - - # L-System Parameters - derivation_length: int = 160 # Number of derivation steps - - # Growth Parameters - thickness_multiplier: float = 1.2 # Multiplier for internode thickness - bud_age_tolerance: float = 0.01 # Tolerance for age-based bud spacing - - # Visualization Parameters - attractor_point_width: int = 10 # Width of attractor points in visualization - -def generate_points_ufo(): - """ - Generate 3D points for the UFO trellis wire structure. - - Creates a linear array of wire attachment points along the x-axis at a fixed - height (z) and depth (y). The points are spaced evenly within the configured - x-range and used to construct the trellis support structure. - - Returns: - list: List of (x, y, z) tuples representing wire attachment points, - where all points share the same y and z coordinates. - - Configuration parameters used: - - ufo_x_range: Tuple (min_x, max_x) defining the range of x coordinates - - ufo_x_spacing: Spacing between consecutive x coordinates - - ufo_z_value: Fixed z-coordinate (height) for all points - - ufo_y_value: Fixed y-coordinate (depth) for all points - """ - x = np.arange( - simulation_config.ufo_x_range[0], - simulation_config.ufo_x_range[1], - simulation_config.ufo_x_spacing - ).astype(float) - z = np.full((x.shape[0],), simulation_config.ufo_z_value).astype(float) - y = np.full((x.shape[0],), simulation_config.ufo_y_value).astype(float) - - wire_attachment_points = [] - for point_index in range(x.shape[0]): - wire_attachment_points.append((x[point_index], y[point_index], z[point_index])) - - return wire_attachment_points - \ No newline at end of file diff --git a/examples/UFO/UFO_simulation.py b/examples/UFO/UFO_simulation.py new file mode 100644 index 0000000..d6a86dc --- /dev/null +++ b/examples/UFO/UFO_simulation.py @@ -0,0 +1,298 @@ +from dataclasses import dataclass +import numpy as np +from helper import cut_from + +@dataclass +class UFOSimulationConfig: + """Configuration for UFO trellis tree simulation parameters.""" + + # Tying and Pruning + num_iteration_tie: int = 8 + num_iteration_prune: int = 16 + + # Display + label: bool = True + + # Support Structure + support_trunk_wire_point: tuple = (0.6, 0, 0.4) + support_num_wires: int = 7 + support_spacing_wires: int = 1 + + # Point Generation + ufo_x_range: tuple = (0.65, 3) + ufo_x_spacing: float = 0.3 + ufo_z_value: float = 1.4 + ufo_y_value: float = 0 + + # Energy and Tying Parameters + energy_distance_weight: float = 0.5 # Weight for distance in energy calculation (was hardcoded /2) + energy_threshold: float = 1.0 # Maximum energy threshold for tying + + # Pruning Parameters + pruning_age_threshold: int = 8 # Age threshold for pruning untied branches + + # L-System Parameters + derivation_length: int = 160 # Number of derivation steps + + # Growth Parameters + thickness_multiplier: float = 1.2 # Multiplier for internode thickness + bud_age_tolerance: float = 0.01 # Tolerance for age-based bud spacing + + # Visualization Parameters + attractor_point_width: int = 10 # Width of attractor points in visualization + +def generate_points_ufo(simulation_config): + """ + Generate 3D points for the UFO trellis wire structure. + + Creates a linear array of wire attachment points along the x-axis at a fixed + height (z) and depth (y). The points are spaced evenly within the configured + x-range and used to construct the trellis support structure. + + Returns: + list: List of (x, y, z) tuples representing wire attachment points, + where all points share the same y and z coordinates. + + Configuration parameters used: + - ufo_x_range: Tuple (min_x, max_x) defining the range of x coordinates + - ufo_x_spacing: Spacing between consecutive x coordinates + - ufo_z_value: Fixed z-coordinate (height) for all points + - ufo_y_value: Fixed y-coordinate (depth) for all points + """ + x = np.arange( + simulation_config.ufo_x_range[0], + simulation_config.ufo_x_range[1], + simulation_config.ufo_x_spacing + ).astype(float) + z = np.full((x.shape[0],), simulation_config.ufo_z_value).astype(float) + y = np.full((x.shape[0],), simulation_config.ufo_y_value).astype(float) + + wire_attachment_points = [] + for point_index in range(x.shape[0]): + wire_attachment_points.append((x[point_index], y[point_index], z[point_index])) + + return wire_attachment_points + + + +def get_energy_mat(branches, arch, simulation_config): + """ + Calculate the energy matrix for optimal branch-to-wire assignment. + + This function computes an energy cost matrix where each entry represents the + "cost" of assigning a specific branch to a specific wire in the trellis system. + The energy is based on the Euclidean distance from wire attachment points to + both the start and end points of each branch, weighted by the simulation's + distance weight parameter. + + The algorithm uses a greedy optimization approach where branches are assigned + to the lowest-energy available wire that hasn't reached capacity. + + Args: + branches: List of branch objects to be assigned to wires + arch: Support architecture object containing wire information + + Returns: + numpy.ndarray: Energy matrix of shape (num_branches, num_wires) where + matrix[i][j] is the energy cost of assigning branch i to wire j. + Untied branches and occupied wires have infinite energy (np.inf). + """ + num_branches = len(branches) + num_wires = len(arch.branch_supports) + + # Initialize energy matrix with infinite values (impossible assignments) + energy_matrix = np.full((num_branches, num_wires), np.inf) + + # Calculate energy costs for all valid branch-wire combinations + for branch_idx, branch in enumerate(branches): + # Skip branches that are already tied + if branch.tying.has_tied: + continue + + for wire_id, wire in arch.branch_supports.items(): + # Skip wires that already have a branch attached + if wire.num_branch >= 1: + continue + + # Calculate weighted distance energy for this branch-wire pair + # Energy considers distance from wire to both branch endpoints + wire_point = np.array(wire.point) + branch_start = np.array(branch.location.start) + branch_end = np.array(branch.location.end) + + start_distance_energy = np.sum((wire_point - branch_start) ** 2) + end_distance_energy = np.sum((wire_point - branch_end) ** 2) + + total_energy = (start_distance_energy + end_distance_energy) * simulation_config.energy_distance_weight + + energy_matrix[branch_idx, wire_id] = total_energy + + return energy_matrix + +def decide_guide(energy_matrix, branches, arch, simulation_config): + """ + Perform greedy assignment of branches to wires based on energy matrix. + + This function implements a greedy optimization algorithm that iteratively assigns + the branch-wire pair with the lowest energy cost. Once a branch is assigned to + a wire, both that branch and wire are marked as unavailable (infinite energy) + to prevent further assignments. + + The algorithm continues until no valid assignments remain (all remaining energies + are infinite or above the threshold). + + Args: + energy_matrix: numpy.ndarray of shape (num_branches, num_wires) with energy costs + branches: List of branch objects to be assigned + arch: Support architecture containing wire information + + Returns: + None: Modifies branches and arch in-place with new assignments + """ + num_branches, num_wires = energy_matrix.shape + + # Early return if no branches or wires to assign + if num_branches == 0 or num_wires == 0: + return + + # Continue making assignments until no valid ones remain + while True: + # Find the minimum energy value and its position + min_energy_indices = np.argwhere(energy_matrix == np.min(energy_matrix)) + + # If no valid indices found or matrix is empty, stop + if len(min_energy_indices) == 0: + break + + # Get the first (and typically only) minimum energy position + branch_idx, wire_id = min_energy_indices[0] + min_energy = energy_matrix[branch_idx, wire_id] + + # Stop if minimum energy is infinite (no valid assignments) or above threshold + if np.isinf(min_energy) or min_energy > simulation_config.energy_threshold: + break + + # Get the branch and wire objects + branch = branches[branch_idx] + wire = arch.branch_supports[wire_id] + + # Skip if branch is already tied (defensive check) + if branch.tying.has_tied: + # Mark this assignment as invalid and continue + energy_matrix[branch_idx, wire_id] = np.inf + continue + + # Perform the assignment + branch.tying.guide_target = wire + wire.add_branch() + + # Mark branch and wire as unavailable for future assignments + # Set entire row (branch) to infinity - this branch can't be assigned again + energy_matrix[branch_idx, :] = np.inf + # Set entire column (wire) to infinity - this wire can't accept more branches + energy_matrix[:, wire_id] = np.inf + + +def prune(lstring, simulation_config): + """ + Prune old branches that exceed the age threshold and haven't been tied to wires. + + This function implements the pruning strategy for the tree training simulation. + It identifies branches that have grown too old (exceeding the pruning age threshold) + but haven't been successfully tied to trellis wires. Such branches are considered + unproductive and are removed from the L-System to encourage new growth. + + The pruning criteria are: + 1. Branch age exceeds the configured pruning threshold + 2. Branch has not been tied to any trellis wire + 3. Branch has not already been marked for cutting + + When a branch meets all criteria, it is: + - Marked as cut (to prevent re-processing) + - Removed from the L-System string using cut_from() + + Args: + lstring: The current L-System string containing modules and their parameters + + Returns: + bool: True if a branch was pruned, False if no eligible branches found + + Note: + This function processes one branch at a time and returns immediately after + pruning a single branch. It should be called repeatedly (e.g., in a while loop) + until no more pruning operations are possible. The cut_from() function handles + the actual removal of the branch and any dependent substructures from the string. + """ + for position, symbol in enumerate(lstring): + # Check if this is a WoodStart module (represents a branch) + if symbol.name == 'WoodStart': + branch = symbol[0].type + + # Check pruning criteria + age_exceeds_threshold = branch.info.age > simulation_config.pruning_age_threshold + not_tied_to_wire = not branch.tying.has_tied + not_already_cut = not branch.info.cut + + # Prune if all criteria are met + if age_exceeds_threshold and not_tied_to_wire and not_already_cut: + # Mark branch as cut to prevent re-processing + branch.info.cut = True + + # Remove the branch from the L-System string + lstring = cut_from(position, lstring) + + return True + + return False + +def tie(lstring, simulation_config): + """ + Perform tying operation on eligible branches in the L-System string. + + This function searches through the L-System string for 'WoodStart' modules that + represent branches ready for tying to trellis wires. It identifies branches that: + 1. Have tying properties (tying attribute exists) + 2. Have a defined tie axis (tie_axis is not None) + 3. Have not been tied yet (tie_updated is False) + 4. Have guide points available for wire attachment + + When an eligible branch is found, it performs the tying operation by: + - Marking the branch as tied (tie_updated = False) + - Adding the branch to the target wire + - Calling the branch's tie_lstring method to modify the L-System string + + Args: + lstring: The current L-System string containing modules and their parameters + + Returns: + bool: True if a tying operation was performed, False if no eligible branches found + + Note: + This function processes one branch at a time and returns immediately after + tying a single branch. It should be called repeatedly (e.g., in a while loop) + until no more tying operations are possible. + """ + for position, symbol in enumerate(lstring): + # Check if this is a WoodStart module with tying capabilities + if (symbol == 'WoodStart' and + hasattr(symbol[0].type, 'tying') and + getattr(symbol[0].type.tying, 'tie_axis', None) is not None): + + branch = symbol[0].type + + # Skip branches that have already been processed for tying + if not branch.tying.tie_updated: + continue + + # Check if branch has guide points for wire attachment + if branch.tying.guide_points: + # Perform the tying operation + branch.tying.tie_updated = False + branch.tying.guide_target.add_branch() + + # Update the L-System string with tying modifications + lstring, modifications_count = branch.tie_lstring(lstring, position) + + return True + + return False \ No newline at end of file diff --git a/examples/Camp_Envy_tie_prune_label.lpy b/examples/legacy/Camp_Envy_tie_prune_label.lpy similarity index 100% rename from examples/Camp_Envy_tie_prune_label.lpy rename to examples/legacy/Camp_Envy_tie_prune_label.lpy diff --git a/examples/Envy_tie_prune_label.lpy b/examples/legacy/Envy_tie_prune_label.lpy similarity index 100% rename from examples/Envy_tie_prune_label.lpy rename to examples/legacy/Envy_tie_prune_label.lpy diff --git a/examples/UFO_tie_prune_label.lpy b/examples/legacy/UFO_tie_prune_label.lpy similarity index 100% rename from examples/UFO_tie_prune_label.lpy rename to examples/legacy/UFO_tie_prune_label.lpy diff --git a/examples/static_envy.lpy b/examples/legacy/static_envy.lpy similarity index 100% rename from examples/static_envy.lpy rename to examples/legacy/static_envy.lpy diff --git a/examples/static_ufo.lpy b/examples/legacy/static_ufo.lpy similarity index 100% rename from examples/static_ufo.lpy rename to examples/legacy/static_ufo.lpy diff --git a/helper.py b/helper.py index 83f0ec8..478d12f 100644 --- a/helper.py +++ b/helper.py @@ -1,352 +1,235 @@ +""" +Helper utilities for L-Py tree simulation system. + +This module provides utility functions for procedural tree generation and simulation +in the L-Py framework. It includes functions for: + +- L-System string manipulation (cutting, pruning operations) +- Geometric shape generation (contours, curves, noise patterns) +- Tree training and optimization utilities +- PlantGL integration for 3D visualization + +The functions in this module are used by various tree architecture implementations +(UFO, Envy, etc.) to perform common operations like branch pruning, wire attachment +optimization, and geometric shape generation for realistic tree modeling. +""" + from openalea.plantgl.all import NurbsCurve, Vector3, Vector4, Point4Array, Point2Array, Point3Array, Polyline2D, BezierCurve, BezierCurve2D from openalea.lpy import Lsystem, newmodule from random import uniform, seed from numpy import linspace, pi, sin, cos import numpy as np -def amplitude(x): return 2 - -def cut_from(pruning_id, s, path = None): - """Check cut_string_from_manipulation for manual implementation""" - # s.insertAt(pruning_id, newmodule('F')) - s.insertAt(pruning_id+1, newmodule('%')) - return s - -def cut_using_string_manipulation(pruning_id, s, path = None): - """Cuts starting from index pruning_id until branch - end signified by ']' or the entire subtrees if pruning_id starts from leader""" - bracket_balance = 0 - cut_num = pruning_id - #s[cut_num].append("no cut") - cut_num += 1 - pruning_id +=1 - total_length = len(s) - while(pruning_id < total_length): - if s[cut_num].name == '[': - bracket_balance+=1 - if s[cut_num].name == ']': - if bracket_balance == 0: - break - else: - bracket_balance-=1 - del s[cut_num] - pruning_id+=1 # Insert new node cut at the end of cut - if path != None: - new_lsystem = Lsystem(path) #Figure out to include time in this - new_lsystem.axiom = s - return new_lsystem - #s.insertAt(cut_num, newmodule("I(1, 0.05)")) - return s - -def pruning_strategy(it, lstring): - if((it+1)%8 != 0): + +def cut_from(pruning_position, lstring, lsystem_path=None): + """ + Mark a position in the L-System string for cutting/pruning. + + Inserts a cut marker (%) after the specified pruning position in the + L-System string. This marks the location where a branch should be + removed during the pruning process. + + Args: + pruning_position: Index in the L-System string where pruning should occur + lstring: The L-System string to modify + lsystem_path: Optional path to create a new L-System object (unused in current implementation) + + Returns: + Modified L-System string with cut marker inserted + """ + # Insert cut marker (%) after the pruning position + lstring.insertAt(pruning_position + 1, newmodule('%')) return lstring - cut = False - curr = 0 - while curr < len(lstring): - if lstring[curr] == '/': - if not (angle_between(lstring[curr].args[0], 0, 50) or angle_between(lstring[curr].args[0], 130, 180)): - if(len(lstring[curr].args) > 1): - if lstring[curr].args[1] == "no cut": - curr+=1 - continue - - # print("Cutting", curr, lstring[curr], (lstring[curr].args[0]+180)) - #lstring[curr].append("no cut") - lstring = cut_from(curr+1, lstring) - elif lstring[curr] == '&': - if not (angle_between(lstring[curr].args[0], 0, 50) or angle_between(lstring[curr].args[0], 130, 180)): - if(len(lstring[curr].args) > 1): - if lstring[curr].args[1] == "no cut": - curr+=1 - continue - # print("Cutting", curr, lstring[curr], (lstring[curr].args[0]+180)) - #lstring[curr].append("no cut") - lstring = cut_from(curr+1, lstring) - curr+=1 - - return lstring -def angle_between(angle, min, max): - angle = (angle+90) - if angle > min and angle < max: - return True - return False - -def myrandom(radius): - return uniform(-radius,radius) - -def gen_noise_branch(radius,nbp=20): - return NurbsCurve([(0,0,0,1),(0,0,1/float(nbp-1),1)]+[(myrandom(radius*amplitude(pt/float(nbp-1))), - myrandom(radius*amplitude(pt/float(nbp-1))), - pt/float(nbp-1),1) for pt in range(2,nbp)], - degree=min(nbp-1,3),stride=nbp*100) - -def create_noisy_branch_contour(radius, noise_factor, num_points=100, seed=None): - if seed is not None: - seed(seed) - t = linspace(0, 2 * pi, num_points, endpoint=False) - points = [] - for angle in t: - # Base circle points - x = radius * cos(angle) - y = radius * sin(angle) - - # Add noise - noise_x = uniform(-noise_factor, noise_factor) - noise_y = uniform(-noise_factor, noise_factor) - - noisy_x = x + noise_x - noisy_y = y + noise_y - - points.append((noisy_x, noisy_y)) - - # Ensure the curve is closed by adding the first point at the end - points.append(points[0]) - - # Create the PlantGL Point2Array and Polyline2D - curve_points = Point2Array(points) - curve = Polyline2D(curve_points) - return curve - -def create_bezier_curve(num_control_points=6, x_range=(-2,2), y_range=(-2, 2), z_range = (0, 10), seed_val=None): - if seed_val is not None: - seed(seed_val) # Set the random seed for reproducibility - # Generate progressive control points within the specified ranges - control_points = [] - zs = linspace(z_range[0], z_range[1], num_control_points) - for i in range(num_control_points): - x = uniform(*x_range) - y = uniform(*y_range) - control_points.append(Vector4(x, y, zs[i], 1)) # Set z to 0 for 2D curve - # Create a Point3Array from the control points - control_points_array = Point4Array(control_points) - # Create and return the BezierCurve2D object - bezier_curve = BezierCurve(control_points_array) - return bezier_curve - - - -def get_energy_mat(branches, arch, simulation_config): +def cut_using_string_manipulation(pruning_position, lstring, lsystem_path=None): """ - Calculate the energy matrix for optimal branch-to-wire assignment. - - This function computes an energy cost matrix where each entry represents the - "cost" of assigning a specific branch to a specific wire in the trellis system. - The energy is based on the Euclidean distance from wire attachment points to - both the start and end points of each branch, weighted by the simulation's - distance weight parameter. - - The algorithm uses a greedy optimization approach where branches are assigned - to the lowest-energy available wire that hasn't reached capacity. - + Remove a complete branch segment from the L-System string. + + Cuts starting from the pruning position until the end of the branch segment, + which is signified by a closing bracket ']'. Uses bracket balancing to handle + nested branch structures correctly. + Args: - branches: List of branch objects to be assigned to wires - arch: Support architecture object containing wire information - + pruning_position: Starting index in the L-System string for the cut operation + lstring: The L-System string to modify + lsystem_path: Optional path to create a new L-System object with the modified string + Returns: - numpy.ndarray: Energy matrix of shape (num_branches, num_wires) where - matrix[i][j] is the energy cost of assigning branch i to wire j. - Untied branches and occupied wires have infinite energy (np.inf). + Modified L-System string with the branch segment removed, or a new L-System + object if lsystem_path is provided """ - num_branches = len(branches) - num_wires = len(arch.branch_supports) - - # Initialize energy matrix with infinite values (impossible assignments) - energy_matrix = np.full((num_branches, num_wires), np.inf) - - # Calculate energy costs for all valid branch-wire combinations - for branch_idx, branch in enumerate(branches): - # Skip branches that are already tied - if branch.tying.has_tied: - continue - - for wire_id, wire in arch.branch_supports.items(): - # Skip wires that already have a branch attached - if wire.num_branch >= 1: - continue - - # Calculate weighted distance energy for this branch-wire pair - # Energy considers distance from wire to both branch endpoints - wire_point = np.array(wire.point) - branch_start = np.array(branch.location.start) - branch_end = np.array(branch.location.end) - - start_distance_energy = np.sum((wire_point - branch_start) ** 2) - end_distance_energy = np.sum((wire_point - branch_end) ** 2) - - total_energy = (start_distance_energy + end_distance_energy) * simulation_config.energy_distance_weight - - energy_matrix[branch_idx, wire_id] = total_energy - - return energy_matrix - -def decide_guide(energy_matrix, branches, arch, simulation_config): + bracket_balance = 0 + current_position = pruning_position + # Skip the pruning position itself + current_position += 1 + search_position = pruning_position + 1 + total_length = len(lstring) + + # Traverse the string until we find the matching closing bracket + while search_position < total_length: + if lstring[current_position].name == '[': + bracket_balance += 1 + elif lstring[current_position].name == ']': + if bracket_balance == 0: + # Found the matching closing bracket, stop here + break + else: + bracket_balance -= 1 + + # Remove the current element + del lstring[current_position] + search_position += 1 + + # If a path is provided, create a new L-System object + if lsystem_path is not None: + new_lsystem = Lsystem(lsystem_path) + new_lsystem.axiom = lstring + return new_lsystem + + return lstring + + +def angle_between(angle, min_angle, max_angle): """ - Perform greedy assignment of branches to wires based on energy matrix. - - This function implements a greedy optimization algorithm that iteratively assigns - the branch-wire pair with the lowest energy cost. Once a branch is assigned to - a wire, both that branch and wire are marked as unavailable (infinite energy) - to prevent further assignments. - - The algorithm continues until no valid assignments remain (all remaining energies - are infinite or above the threshold). - + Check if an angle falls within a specified range after 90-degree offset. + + Applies a 90-degree offset to the input angle and checks if the result + falls within the specified range. This is used for determining acceptable + tropism angles in the pruning strategy. + Args: - energy_matrix: numpy.ndarray of shape (num_branches, num_wires) with energy costs - branches: List of branch objects to be assigned - arch: Support architecture containing wire information - + angle: Input angle in degrees + min_angle: Minimum angle of the acceptable range (after offset) + max_angle: Maximum angle of the acceptable range (after offset) + Returns: - None: Modifies branches and arch in-place with new assignments + bool: True if the offset angle is within the range, False otherwise """ - num_branches, num_wires = energy_matrix.shape - - # Early return if no branches or wires to assign - if num_branches == 0 or num_wires == 0: - return - - # Continue making assignments until no valid ones remain - while True: - # Find the minimum energy value and its position - min_energy_indices = np.argwhere(energy_matrix == np.min(energy_matrix)) - - # If no valid indices found or matrix is empty, stop - if len(min_energy_indices) == 0: - break - - # Get the first (and typically only) minimum energy position - branch_idx, wire_id = min_energy_indices[0] - min_energy = energy_matrix[branch_idx, wire_id] - - # Stop if minimum energy is infinite (no valid assignments) or above threshold - if np.isinf(min_energy) or min_energy > simulation_config.energy_threshold: - break - - # Get the branch and wire objects - branch = branches[branch_idx] - wire = arch.branch_supports[wire_id] - - # Skip if branch is already tied (defensive check) - if branch.tying.has_tied: - # Mark this assignment as invalid and continue - energy_matrix[branch_idx, wire_id] = np.inf - continue - - # Perform the assignment - branch.tying.guide_target = wire - wire.add_branch() - - # Mark branch and wire as unavailable for future assignments - # Set entire row (branch) to infinity - this branch can't be assigned again - energy_matrix[branch_idx, :] = np.inf - # Set entire column (wire) to infinity - this wire can't accept more branches - energy_matrix[:, wire_id] = np.inf - - -def prune(lstring, simulation_config): + offset_angle = angle + 90 + return min_angle <= offset_angle <= max_angle + +def generate_random_offset(radius): """ - Prune old branches that exceed the age threshold and haven't been tied to wires. + Generate a random offset value within a specified radius range. + + Creates a random float value between -radius and +radius, useful for + adding noise or variation to geometric shapes and curves. - This function implements the pruning strategy for the tree training simulation. - It identifies branches that have grown too old (exceeding the pruning age threshold) - but haven't been successfully tied to trellis wires. Such branches are considered - unproductive and are removed from the L-System to encourage new growth. + Args: + radius: Maximum absolute value for the random offset - The pruning criteria are: - 1. Branch age exceeds the configured pruning threshold - 2. Branch has not been tied to any trellis wire - 3. Branch has not already been marked for cutting + Returns: + float: Random value between -radius and +radius + """ + return uniform(-radius, radius) + +def generate_noisy_branch_curve(radius, num_control_points=20): + """ + Generate a NURBS curve representing a noisy branch shape. - When a branch meets all criteria, it is: - - Marked as cut (to prevent re-processing) - - Removed from the L-System string using cut_from() + Creates a 3D NURBS curve with noise applied to create a natural-looking + branch shape. The curve starts at the origin and extends along the z-axis, + with x and y coordinates perturbed by noise that scales with distance. Args: - lstring: The current L-System string containing modules and their parameters + radius: Base radius for noise generation + num_control_points: Number of control points for the NURBS curve Returns: - bool: True if a branch was pruned, False if no eligible branches found + NurbsCurve: PlantGL NURBS curve object representing the noisy branch + """ + # Create control points with progressive noise + control_points = [(0, 0, 0, 1), (0, 0, 1/float(num_control_points-1), 1)] + + for point_index in range(2, num_control_points): + t = point_index / float(num_control_points - 1) + noise_scale = radius * 2 # amplitude scaling factor + + x_noise = generate_random_offset(noise_scale) + y_noise = generate_random_offset(noise_scale) + + control_points.append((x_noise, y_noise, t, 1)) - Note: - This function processes one branch at a time and returns immediately after - pruning a single branch. It should be called repeatedly (e.g., in a while loop) - until no more pruning operations are possible. The cut_from() function handles - the actual removal of the branch and any dependent substructures from the string. + return NurbsCurve(control_points, degree=min(num_control_points-1, 3), stride=num_control_points*100) + +def create_noisy_branch_contour(radius, noise_factor, num_points=100, seed_value=None): """ - for position, symbol in enumerate(lstring): - # Check if this is a WoodStart module (represents a branch) - if symbol.name == 'WoodStart': - branch = symbol[0].type + Create a noisy 2D contour for branch cross-sections. - # Check pruning criteria - age_exceeds_threshold = branch.info.age > simulation_config.pruning_age_threshold - not_tied_to_wire = not branch.tying.has_tied - not_already_cut = not branch.info.cut + Generates a circular contour with added noise to create natural-looking + branch cross-section shapes. The contour is closed and can be used for + extruding 3D branch geometry. - # Prune if all criteria are met - if age_exceeds_threshold and not_tied_to_wire and not_already_cut: - # Mark branch as cut to prevent re-processing - branch.info.cut = True + Args: + radius: Base radius of the circular contour + noise_factor: Scale factor for the noise added to the contour + num_points: Number of points in the contour (higher = smoother) + seed_value: Random seed for reproducible results - # Remove the branch from the L-System string - lstring = cut_from(position, lstring) + Returns: + Polyline2D: PlantGL 2D polyline representing the noisy contour + """ + if seed_value is not None: + seed(seed_value) + + # Generate angles around the circle + angles = linspace(0, 2 * pi, num_points, endpoint=False) + contour_points = [] + + for angle in angles: + # Calculate base circle coordinates + x_base = radius * cos(angle) + y_base = radius * sin(angle) + + # Add noise to create irregular shape + x_noise = uniform(-noise_factor, noise_factor) + y_noise = uniform(-noise_factor, noise_factor) - return True + x_noisy = x_base + x_noise + y_noisy = y_base + y_noise - return False + contour_points.append((x_noisy, y_noisy)) + # Close the contour by repeating the first point + contour_points.append(contour_points[0]) + # Create PlantGL geometry + point_array = Point2Array(contour_points) + return Polyline2D(point_array) -def tie(lstring, simulation_config): +def create_bezier_curve(num_control_points=6, x_range=(-2, 2), y_range=(-2, 2), z_range=(0, 10), seed_value=None): """ - Perform tying operation on eligible branches in the L-System string. - - This function searches through the L-System string for 'WoodStart' modules that - represent branches ready for tying to trellis wires. It identifies branches that: - 1. Have tying properties (tying attribute exists) - 2. Have a defined tie axis (tie_axis is not None) - 3. Have not been tied yet (tie_updated is False) - 4. Have guide points available for wire attachment - - When an eligible branch is found, it performs the tying operation by: - - Marking the branch as tied (tie_updated = False) - - Adding the branch to the target wire - - Calling the branch's tie_lstring method to modify the L-System string - + Create a randomized 3D Bezier curve for growth guidance. + + Generates a Bezier curve with randomly positioned control points within + specified ranges. The curve progresses along the z-axis with control points + distributed evenly in the z-direction but randomly in x and y. + Args: - lstring: The current L-System string containing modules and their parameters - + num_control_points: Number of control points for the Bezier curve + x_range: Tuple (min_x, max_x) defining the x-coordinate range + y_range: Tuple (min_y, max_y) defining the y-coordinate range + z_range: Tuple (min_z, max_z) defining the z-coordinate range + seed_value: Random seed for reproducible curve generation + Returns: - bool: True if a tying operation was performed, False if no eligible branches found - - Note: - This function processes one branch at a time and returns immediately after - tying a single branch. It should be called repeatedly (e.g., in a while loop) - until no more tying operations are possible. + BezierCurve: PlantGL Bezier curve object for growth guidance """ - for position, symbol in enumerate(lstring): - # Check if this is a WoodStart module with tying capabilities - if (symbol == 'WoodStart' and - hasattr(symbol[0].type, 'tying') and - getattr(symbol[0].type.tying, 'tie_axis', None) is not None): - - branch = symbol[0].type - - # Skip branches that have already been processed for tying - if not branch.tying.tie_updated: - continue - - # Check if branch has guide points for wire attachment - if branch.tying.guide_points: - # Perform the tying operation - branch.tying.tie_updated = False - branch.tying.guide_target.add_branch() - - # Update the L-System string with tying modifications - lstring, modifications_count = branch.tie_lstring(lstring, position) - - return True - - return False \ No newline at end of file + if seed_value is not None: + seed(seed_value) + + # Generate control points with progressive z-coordinates + z_values = linspace(z_range[0], z_range[1], num_control_points) + control_points = [] + + for z_value in z_values: + x_coord = uniform(x_range[0], x_range[1]) + y_coord = uniform(y_range[0], y_range[1]) + control_points.append(Vector4(x_coord, y_coord, z_value, 1)) + + # Create PlantGL Bezier curve + control_point_array = Point4Array(control_points) + return BezierCurve(control_point_array) + + + \ No newline at end of file From a2070f0cb62f334f1279ef71dca9f326eaba91cc Mon Sep 17 00:00:00 2001 From: Abhinav Jain Date: Wed, 12 Nov 2025 13:58:49 -0800 Subject: [PATCH 08/14] Updated docs and post_bud, pre_bud functions --- bug_test.lpy | 307 --------------------------------- docs/source/conf.py | 1 - docs/source/files.rst | 60 +++++-- docs/source/index.rst | 32 ++-- docs/source/installation.rst | 58 +++++-- docs/source/methods.rst | 30 ++++ docs/source/resources.rst | 25 ++- docs/source/usage.rst | 75 ++++++-- examples/UFO/UFO.lpy | 31 ++-- examples/UFO/UFO_prototypes.py | 30 ++++ examples/UFO/UFO_simulation.py | 2 +- helper.py | 5 +- stochastic_tree.py | 12 ++ 13 files changed, 284 insertions(+), 384 deletions(-) delete mode 100644 bug_test.lpy diff --git a/bug_test.lpy b/bug_test.lpy deleted file mode 100644 index ff30ad8..0000000 --- a/bug_test.lpy +++ /dev/null @@ -1,307 +0,0 @@ -""" -Tying, Pruning, and Labelling Envy architecture tree -""" -import sys -sys.path.append('../') -from stochastic_tree import Support, BasicWood -import numpy as np -import random as rd -import copy -import gc -import time - -from helper import * - -class Spur(BasicWood): - count = 0 - def __init__(self, copy_from = None, max_buds_segment: int = 5, thickness: float = 0.1,\ - thickness_increment: float = 0.01, growth_length: float = 1., max_length: float = 7.,\ - tie_axis: list = [0,1,1], order: int = 1, prototype_dict: dict = {}, name = None, color = None): - super().__init__(copy_from, max_buds_segment,thickness, thickness_increment, growth_length,\ - max_length, tie_axis, order, color) - if copy_from: - self.__copy_constructor__(copy_from) - else: - self.prototype_dict = prototype_dict - if not name: - self.name = str(self.__class__.__name__) +'_'+ str(self.__class__.count) - Spur.count+=1 - - def is_bud_break(self, num_buds_segment): - return (rd.random() < 0.1) - - def create_branch(self): - return None - - def grow(self): - pass - -class Branch(BasicWood): - count = 0 - def __init__(self, copy_from = None, max_buds_segment: int = 5, thickness: float = 0.1,\ - thickness_increment: float = 0.01, growth_length: float = 1., max_length: float = 7.,\ - tie_axis: tuple = (0,1,1), order: int = 1, prototype_dict: dict = {}, name = None, color = None): - - super().__init__(copy_from, max_buds_segment,thickness, thickness_increment, growth_length,\ - max_length, tie_axis, order, color) - if copy_from: - self.__copy_constructor__(copy_from) - else: - self.prototype_dict = prototype_dict - - if not name: - self.name = str(self.__class__.__name__) +'_'+ str(self.__class__.count) - Branch.count+=1 - - def is_bud_break(self, num_buds_segment): - return (rd.random() < 0.02*(1 - num_buds_segment/self.max_buds_segment)) - - def create_branch(self): - new_ob = Spur(copy_from = self.prototype_dict['spur']) - return new_ob - - def grow(self): - pass - -class NonTrunk(BasicWood): - count = 0 - def __init__(self, copy_from = None, max_buds_segment: int = 5, thickness: float = 0.1,\ - thickness_increment: float = 0.01, growth_length: float = 1., max_length: float = 7.,\ - tie_axis: tuple = (0,1,1), order: int = 1, prototype_dict: dict = {}, name = None, color = None): - - super().__init__(copy_from, max_buds_segment,thickness, thickness_increment, growth_length,\ - max_length, tie_axis, order, color) - if copy_from: - self.__copy_constructor__(copy_from) - else: - self.prototype_dict = prototype_dict - - if not name: - self.name = str(self.__class__.__name__) +'_'+ str(self.__class__.count) - Branch.count+=1 - - def is_bud_break(self, num_buds_segment): - return (rd.random() < 0.02*(1 - num_buds_segment/self.max_buds_segment)) - - def create_branch(self): - new_ob = Spur(copy_from = self.prototype_dict['spur']) - return new_ob - - def grow(self): - pass - - -class Trunk(BasicWood): - count = 0 - """ Details of the trunk while growing a tree, length, thickness, where to attach them etc """ - def __init__(self, copy_from = None, max_buds_segment: int = 5, thickness: float = 0.1,\ - thickness_increment: float = 0.01, growth_length: float = 1., max_length: float = 7.,\ - tie_axis: tuple = (0,1,1), order: int = 0, prototype_dict: dict = {}, name = None, color = None): - - super().__init__(copy_from, max_buds_segment,thickness, thickness_increment, growth_length,\ - max_length, tie_axis, order, color) - if copy_from: - self.__copy_constructor__(copy_from) - else: - self.prototype_dict = prototype_dict - if not name: - self.name = str(self.__class__.__name__) +'_'+ str(self.__class__.count) - Trunk.count+=1 - - def is_bud_break(self, num_buds_segment): - if (rd.random() > 0.02*(1 - num_buds_segment/self.max_buds_segment)): - return False - return True - - def create_branch(self): - if rd.random() > 0.8: - return Spur(copy_from = self.prototype_dict['spur']) - else: - return Branch(copy_from = self.prototype_dict['branch']) - - def grow(self): - pass - - - - -#Pass transition probabs? --> solve with abstract classes - -#basicwood_prototypes = {} -#basicwood_prototypes['trunk'] = Trunk(tie_axis = [0,1,1], max_length = 20, thickness_increment = 0.02, prototype_dict = basicwood_prototypes, color = 0) -#basicwood_prototypes['branch'] = Branch(tie_axis = [0,1,1], max_length = 20, thickness_increment = 0.005, prototype_dict = basicwood_prototypes, color = 1) -#basicwood_prototypes['spur'] = Spur(tie_axis = [0,1,1], max_length = 1, thickness_increment = 0.005, prototype_dict = basicwood_prototypes, color = 2) - -growth_length = 0.1 -#everything is relative to growth length -basicwood_prototypes = {} -basicwood_prototypes['trunk'] = Trunk(tie_axis = (0,1,1), max_length = 2.5/growth_length, thickness = 0.01, growth_length = 0.1,thickness_increment = 0.0005, prototype_dict = basicwood_prototypes, color = 0) -basicwood_prototypes['branch'] = Branch(tie_axis = (0,1,1), max_length = .45/growth_length, thickness = 0.01, growth_length = 0.1,thickness_increment = 0.00005, prototype_dict = basicwood_prototypes, color = 1) -basicwood_prototypes['nontrunkbranch'] = NonTrunk(tie_axis = (0,0,1), max_length = 0.1/growth_length, growth_length = 0.1, thickness = 0.0001,thickness_increment = 0.0001, prototype_dict = basicwood_prototypes, color = 1) -basicwood_prototypes['spur'] = Spur(tie_axis = (0,1,1), max_length = 0.01/growth_length, thickness = 0.005, growth_length = 0.01,thickness_increment = 0., prototype_dict = basicwood_prototypes, color = 2) - -#init -trunk_base = Trunk(copy_from = basicwood_prototypes['trunk']) -time_count = 0 -label = True -def generate_points_v_trellis(): - x = np.full((7,), 1.45).astype(float) - #z = np.arange(3, 24, 3).astype(float) - y = np.full((7,), 0).astype(float) - z = np.arange(0.6,3.4, 0.45) - pts = [] - id = 0 - for i in range(x.shape[0]): - pts.append((-x[i], y[i], z[i])) - id+=1 - pts.append((x[i], y[i], z[i])) - id+=1 - return pts - - - -support = Support(generate_points_v_trellis(), 14 , 1 , None, (0,0,1), None) -num_iteration_tie = 30 - -###Tying stuff begins - -def ed(a,b): - return np.linalg.norm(a-b) - -def get_energy_mat(branches, arch): - #branches = [i for i in branches if "Branch" in i.name] - num_branches = len(branches) - num_wires = len(list(arch.branch_supports.values())) - energy_matrix = np.ones((num_branches,num_wires))*np.inf - #print(energy_matrix.shape) - for branch_id, branch in enumerate(branches): - if branch.has_tied: - continue - for wire_id, wire in arch.branch_supports.items(): - if wire.num_branch>=1: - continue - energy_matrix[branch_id][wire_id] = ed(wire.point,branch.end)/2+ed(wire.point,branch.start)/2#+v.num_branches*10+branch.bend_energy(deflection, curr_branch.age) - return energy_matrix - -def decide_guide(energy_matrix, branches, arch): - for i in range(energy_matrix.shape[0]): - min_arg = np.argwhere(energy_matrix == np.min(energy_matrix)) - #print(min_arg) - if(energy_matrix[min_arg[0][0]][min_arg[0][1]] == np.inf):# or energy_matrix[min_arg[0][0]][min_arg[0][1]] > 1: - return - if not (branches[min_arg[0][0]].has_tied == True):# and not (arch.branch_supports[min_arg[0][1]].num_branch >=1): - #print("Imp:",min_arg[0][0], min_arg[0][1], energy_matrix[min_arg[0][0]][min_arg[0][1]]) - branches[min_arg[0][0]].guide_target = arch.branch_supports[min_arg[0][1]]#copy.deepcopy(arch.branch_supports[min_arg[0][1]].point) - #trellis_wires.trellis_pts[min_arg[0][1]].num_branches+=1 - for j in range(energy_matrix.shape[1]): - energy_matrix[min_arg[0][0]][j] = np.inf - for j in range(energy_matrix.shape[0]): - energy_matrix[j][min_arg[0][1]] = np.inf - -def tie(lstring): - for j,i in enumerate(lstring): - if i == 'C' and i[0].type.__class__.__name__ == 'Branch': - if i[0].type.tie_updated == False: - continue - curr = i[0] - if i[0].type.guide_points: - #print("tying ", i[0].type.name, i[0].type.guide_target.point) - i[0].type.tie_updated = False - i[0].type.guide_target.add_branch() - lstring, count = i[0].type.tie_lstring(lstring, j) - - return True - return False - -#Pruning strategy - -def pruning_strategy(lstring): #Remove remnants of cut - cut = False - - for j,i in enumerate(lstring): - - if i.name == 'C' and i[0].type.age > 10 and i[0].type.has_tied == False and i[0].type.cut == False: - - i[0].type.cut = True - #print("Cutting", i[0].type.name) - lstring = cut_from(j, lstring) - - return True - - return False - -def StartEach(lstring): - global parent_child_dict - for i in parent_child_dict[trunk_base.name]: - if i.tie_updated == False: - i.tie_update() - - -def EndEach(lstring): - global parent_child_dict, support - tied = False - - if (getIterationNb()+1)%num_iteration_tie == 0: - energy_matrix = get_energy_mat(parent_child_dict[trunk_base.name], support) - print(energy_matrix) - decide_guide(energy_matrix, parent_child_dict[trunk_base.name], support) - for branch in parent_child_dict[trunk_base.name]: - branch.update_guide(branch.guide_target) - print(branch.name, branch.guide_target) - while tie(lstring): - pass - while pruning_strategy(lstring): - pass - return lstring - -parent_child_dict = {} -parent_child_dict[trunk_base.name] = [] -#print(generate_points_ufo()) -module Attractors -module grow_object -module bud -module branch -module C -Axiom: Attractors(support)grow_object(trunk_base) -derivation length: 100 - -production: -#Decide whether branch internode vs trunk internode need to be the same size. -grow_object(o) : - if o == None: - produce * - if o.length >= o.max_length: - o.age+=1 - nproduce * - else: - if label: - nproduce SetColor(o.color) - o.grow_one() - produce I(o.growth_length, o.thickness, o)bud(ParameterSet(type = o, num_buds = 0))grow_object(o) - -bud(t) : - if t.type.is_bud_break(t.num_buds): - new_object = t.type.create_branch() - if new_object == None: - produce * - parent_child_dict[new_object.name] = [] - parent_child_dict[t.type.name].append(new_object) - #Store new object somewhere - t.num_buds+=1 - t.type.num_branches+=1 - nproduce [@RGetPos(new_object.start)C(ParameterSet(type = new_object))/(rd.random()*360)&(rd.random()*90)grow_object(new_object)GetPos(new_object.end)]bud(t) - - -I(s,r,o) --> I(s,r+o.thickness_increment, o) -_(r) --> _(r+o.thickness_increment) - -homomorphism: - -I(a,r,o) --> F(a,r) -S(a,r,o) --> F(a,r) - -production: -Attractors(support): - pttodisplay = support.attractor_grid.get_enabled_points() - if len(pttodisplay) > 0: - produce [,(3) @g(PointSet(pttodisplay,width=10))] diff --git a/docs/source/conf.py b/docs/source/conf.py index 602737b..9516d38 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -62,7 +62,6 @@ # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = None # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. diff --git a/docs/source/files.rst b/docs/source/files.rst index 09e5bd1..d83e1e2 100644 --- a/docs/source/files.rst +++ b/docs/source/files.rst @@ -1,17 +1,51 @@ -========= -Files -========= +Files and Directory Structure +============================= -This page gives a brief info of all the files provided in treesim_lpy repository +This document provides an overview of the important files and directories in the `lpy_treesim` project. -* examples/* - * examples/static_envy.lpy - Grows a tree in the envy architecture following predefined rules. No thinning and/or tying takes place. - * examples/static_ufo.lpy - Grows a tree in the ufo architecture following predefined rules. No thinning and/or tying takes place. - * examples/UFO_tie_prune_label.lpy - This file allows the growth of a tree in ufo architecture following the given thinning and tying rules. Further has the option to label different segments as well - * examples/Envy_tie_prune_label.lpy - This file allows the growth of a tree in ufo architecture following the given thinning and tying rules. Further has the option to label different segments as well +Top-Level Files +--------------- -* modules_test/* - * All files in this folder use classes/functions defined in stochastic_tree.py. They can be a good example on how to use the BasicWood, Wire and Support classes +- **`README.rst`**: The main README file for the project. +- **`helper.py`**: Contains helper functions used by other scripts in the project. +- **`stochastic_tree.py`**: The core module for generating stochastic trees. +- **`.gitignore`**: A file that specifies which files and directories to ignore in a Git repository. -* other_files/* - * These files may or may not work. These were used in previous iterations of treesim_lpy. Kept to be used as a reference. \ No newline at end of file +`docs/` +------- + +This directory contains the documentation for the project. + +- **`Makefile`**: A makefile with commands to build the documentation. +- **`source/`**: The source files for the documentation, written in reStructuredText. + - **`conf.py`**: The configuration file for Sphinx, the documentation generator. + - **`index.rst`**: The main entry point for the documentation. + - **`installation.rst`**: Instructions on how to install the project. + - **`usage.rst`**: An explanation of how to use the project. + - **`files.rst`**: An overview of the important files and directories in the project. + - **`resources.rst`**: A list of resources related to the project. + - **`methods.rst`**: A description of the methods used in the project. +- **`_static/`**: Static files, such as images and videos, that are used in the documentation. + +`examples/` +----------- + +This directory contains example `.lpy` files that demonstrate how to use `lpy_treesim`. + +- **`legacy/`**: Older example files. +- **`UFO/`**: Examples related to the UFO cherry tree architecture. + +`modules_test/` +--------------- + +This directory contains test files for the modules in the project. The files in this folder use classes and functions defined in `stochastic_tree.py` and can be a good example of how to use the `BasicWood`, `Wire`, and `Support` classes. + +`other_files/` +-------------- + +These files may or may not work. They were used in previous iterations of `lpy_treesim` and are kept for reference. + +`tree_generation/` +------------------ + +This directory contains scripts for generating and converting tree models. \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst index 6a1015a..fc31c7f 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,25 +1,25 @@ +.. lpy_treesim documentation master file, created by + sphinx-quickstart on Tue Jul 16 11:58:11 2024. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. -TreeSim_Lpy documentation -=================================== - -TreeSim_Lpy is a tree growing simulator based upon L-py (Fred Boudon). Using TreeSim you can perform various tasks -like growing/pruning and tying trees as different architectures, with examples for a UFO cherry and Envy apple provided. -Check out the :doc:`usage` section for further information, including -how to :doc:`installation` the project. - -.. note:: - - This project is under active development. - -Contents --------- +Welcome to lpy_treesim's documentation! +======================================= .. toctree:: + :maxdepth: 2 + :caption: Contents: - Home installation usage files resources methods - + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/source/installation.rst b/docs/source/installation.rst index c043691..51d5b21 100644 --- a/docs/source/installation.rst +++ b/docs/source/installation.rst @@ -1,45 +1,65 @@ Installation ============== -Installing L-Py -*************** - -``L-Py`` distribution is based on the ``conda`` software environment management system. -To install conda, you may refer to its installation page: https://docs.conda.io/projects/conda/en/latest/user-guide/install/ +This document provides instructions on how to install `lpy_treesim` and its dependencies. +Prerequisites +------------- -Installing binaries using conda +- **Python 3.x**: Make sure you have a working Python 3 installation. +- **Conda**: `lpy_treesim` relies on the Conda package manager to handle its environment and dependencies. If you don't have Conda, you can install it by following the official documentation: https://docs.conda.io/projects/conda/en/latest/user-guide/install/ +Installing L-Py +--------------- +`lpy_treesim` is built on top of L-Py, a Python-based L-system simulator. To install L-Py, follow these steps: -To install L-Py, you need to create an environment (named lpy in this case) : +1. **Create a Conda Environment**: Open your terminal and create a new Conda environment named `lpy`: -.. code-block:: bash + .. code-block:: bash conda create -n lpy openalea.lpy -c fredboudon -c conda-forge -The package is retrieved from the ``fredboudon`` channel (developement) and its dependencies will be taken from ``conda-forge`` channel. +2. **Activate the Environment**: Activate the newly created environment: -Then, you need to activate the L-Py environment - -.. code-block:: bash + .. code-block:: bash conda activate lpy -And then run L-Py +3. **Run L-Py**: You can now run L-Py to ensure it's installed correctly: -.. code-block:: bash + .. code-block:: bash lpy -For any issues with L-py, please check the documentation of L-Py provided here https://lpy.readthedocs.io/en/latest/user/installing.html +For more detailed information and troubleshooting, refer to the official L-Py documentation: https://lpy.readthedocs.io/en/latest/user/installing.html + +Installing `lpy_treesim` +------------------------ +Once you have L-Py set up, you can install `lpy_treesim`: -Installing TreeSim_Lpy -*********************** +1. **Clone the Repository**: Clone this repository to your local machine: -With the conda environment for L-Py set, next we need to clone the TreeSim_Lpy repository. To do that run + .. code-block:: bash + + git clone https://github.com/your-username/lpy_treesim.git + cd lpy_treesim + +2. **Install Dependencies**: The required Python packages are listed in the `requirements.txt` file. You can install them using pip: + + .. code-block:: bash + + pip install -r requirements.txt + +Running the Examples +-------------------- + +The `examples` directory contains several examples that demonstrate how to use `lpy_treesim`. To run an example, navigate to the `examples` directory and run the desired script: .. code-block:: bash - git clone https://github.com/OSUrobotics/treesim_lpy.git \ No newline at end of file + cd examples + python example_script.py + +Replace `example_script.py` with the actual name of the example you want to run. \ No newline at end of file diff --git a/docs/source/methods.rst b/docs/source/methods.rst index 5d02cde..75c7223 100644 --- a/docs/source/methods.rst +++ b/docs/source/methods.rst @@ -1,2 +1,32 @@ +Methods +======= + +This document describes the methods and algorithms used in the `lpy_treesim` project. + +L-System for Tree Generation +---------------------------- + +`lpy_treesim` uses the L-Py language to define the growth rules of the trees. L-Py is a Python-based implementation of L-systems, which are a type of formal grammar that can be used to model the growth of plants and other biological systems. + +The L-system used in `lpy_treesim` is defined in the `.lpy` files in the `examples` directory. These files contain the axiom and production rules that determine the structure of the tree. + +- **Axiom**: The axiom is the initial state of the L-system. It is a string of symbols that represents the starting point of the tree. +- **Production Rules**: The production rules are a set of rules that specify how the symbols in the L-system are replaced over time. Each rule consists of a predecessor and a successor. The predecessor is a symbol that is replaced by the successor. + +By iteratively applying the production rules to the axiom, the L-system generates a sequence of strings that represents the growth of the tree. + +Pruning and Tying Algorithms +---------------------------- + +`lpy_treesim` includes algorithms for pruning and tying the branches of the tree. These algorithms are used to control the shape and size of the tree. + +- **Pruning**: The pruning algorithm removes branches from the tree that are too long or that are growing in the wrong direction. This is done by defining a set of rules that specify which branches to remove. +- **Tying**: The tying algorithm connects branches of the tree together. This is done by defining a set of rules that specify which branches to connect and how to connect them. + +The pruning and tying algorithms are implemented in the `stochastic_tree.py` module. + +API Reference +------------- + .. automodule:: stochastic_tree :members: \ No newline at end of file diff --git a/docs/source/resources.rst b/docs/source/resources.rst index 701e438..ab02936 100644 --- a/docs/source/resources.rst +++ b/docs/source/resources.rst @@ -1,4 +1,23 @@ +Resources +========= -* TreeSim_Lpy Documentation - https://treesim-lpy.readthedocs.io/en/latest/ -* Lpy documentation - https://lpy.readthedocs.io/en/latest -* Lpy training material - https://github.com/fredboudon/lpy-training \ No newline at end of file +This page provides a list of resources related to the `lpy_treesim` project. + +Project Links +------------- + +- **GitHub Repository**: `https://github.com/your-username/lpy_treesim `_ +- **Documentation**: `https://lpy_treesim.readthedocs.io/en/latest/ `_ + +L-Py Resources +-------------- + +- **L-Py Documentation**: `https://lpy.readthedocs.io/en/latest/ `_ +- **L-Py Training Material**: `https://github.com/fredboudon/lpy-training `_ +- **L-Py Paper**: `https://example.com/lpy-paper `_ (Replace with the actual link to the L-Py paper) + +Other Resources +--------------- + +- **Conda Documentation**: `https://docs.conda.io/en/latest/ `_ +- **Sphinx Documentation**: `https://www.sphinx-doc.org/en/master/ `_ \ No newline at end of file diff --git a/docs/source/usage.rst b/docs/source/usage.rst index ba3300b..a1e5fc1 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -1,18 +1,71 @@ -====== Usage -====== +===== -.. code-block:: bash +This section provides a guide on how to use `lpy_treesim` to generate and simulate tree growth. - conda activate lpy - lpy +Running Simulations +------------------- -Once you have launched the lpy GUI, navigate to whatever .lpy file you want to run and press Animate on the toolbar +To run a simulation, you need to have an L-Py file (`.lpy`) that defines the growth rules of the tree. You can find several examples in the `examples` directory. -.. note:: - The tying/pruning processes will only work if you press Animate and not Run +1. **Activate the Conda Environment**: -.. warning:: - It is a known bug that sometimes, the files dont run properly when animated the first time. Press Rewind, and then press Animate to run it again. + .. code-block:: bash - + conda activate lpy + +2. **Launch L-Py**: + + .. code-block:: bash + + lpy + +3. **Open an L-Py File**: In the L-Py GUI, navigate to the desired `.lpy` file and open it. + +4. **Run the Simulation**: + - Click the **Animate** button on the toolbar to start the simulation. + - **Note**: The tying and pruning processes will only work when you use **Animate**, not **Run**. + - **Warning**: There is a known bug where files may not run correctly on the first attempt. If this happens, click **Rewind** and then **Animate** again. + +Defining Growth Rules +--------------------- + +Growth rules are defined using the L-Py language in `.lpy` files. These files typically contain: + +- **Axiom**: The initial state of the L-system. +- **Productions**: A set of rules that define how the L-system evolves over time. + +Here's a simple example of an L-Py file: + +.. code-block:: python + + axiom: A(1) + production: + A(x) -> F(x) A(x+1) + +This example defines a simple L-system that grows a branch of increasing length. + +Visualizing the Results +----------------------- + +L-Py provides a 3D viewer that allows you to visualize the tree as it grows. You can interact with the 3D model by rotating, panning, and zooming. + +Key Modules in `lpy_treesim` +---------------------------- + +`lpy_treesim` is organized into several modules, each with a specific purpose: + +- **`stochastic_tree.py`**: This module contains the core logic for generating stochastic trees. It uses random parameters to create variations in the tree structure. +- **`helper.py`**: This module provides a set of helper functions that are used throughout the package. +- **`tree_generation/`**: This directory contains scripts for generating and converting tree models. + +To use these modules, you can import them into your L-Py files or other Python scripts. For example: + +.. code-block:: python + + from stochastic_tree import generate_stochastic_tree + + # Generate a stochastic tree with custom parameters + tree = generate_stochastic_tree(num_branches=10, branch_length=0.5) + +For more detailed examples, please refer to the scripts in the `examples` directory. diff --git a/examples/UFO/UFO.lpy b/examples/UFO/UFO.lpy index e84e49d..9396790 100644 --- a/examples/UFO/UFO.lpy +++ b/examples/UFO/UFO.lpy @@ -11,7 +11,7 @@ import random as rd from examples.UFO.UFO_prototypes import basicwood_prototypes, Trunk from examples.UFO.UFO_simulation import UFOSimulationConfig, generate_points_ufo, get_energy_mat, decide_guide, tie, prune from dataclasses import dataclass - +import time #init main_trunk = Trunk(copy_from = basicwood_prototypes['trunk']) @@ -188,21 +188,29 @@ grow_object(plant_segment) : else: # Continue growing - update segment properties nproduce SetContour(plant_segment.contour) + #Update internal state of the plant segment plant_segment.grow_one() + #Update physical representation if enable_color_labeling: - # Add color visualization if labeling is enabled + # Add color visualization if labeling is enabled -- Can this be moved to the start of building the segment? r, g, b = plant_segment.info.color nproduce SetColor(r,g,b) - if 'Spur' in plant_segment.name: - # Spur branches: produce short shoots with terminal buds - produce I(plant_segment.growth.growth_length, plant_segment.growth.thickness, plant_segment)bud(ParameterSet(type = plant_segment, num_buds = 0))@O(plant_segment.growth.thickness*simulation_config.thickness_multiplier)grow_object(plant_segment) - else: - # Regular branches: produce internode segment - nproduce I(plant_segment.growth.growth_length, plant_segment.growth.thickness, plant_segment) - if np.isclose(plant_segment.info.age % plant_segment.bud_spacing_age, 0, atol=simulation_config.bud_age_tolerance): + #Produce internode segment + nproduce I(plant_segment.growth.growth_length, plant_segment.growth.thickness, plant_segment) + #Produce bud + if plant_segment.pre_bud_rule(plant_segment, simulation_config): + for module in plant_segment.post_bud_rule(plant_segment, simulation_config): + nproduce new(module[0], *module[1]) + + if should_bud(plant_segment, simulation_config): # Age-based bud production for lateral branching nproduce bud(ParameterSet(type = plant_segment, num_buds = 0)) - produce grow_object(plant_segment) + + if plant_segment.post_bud_rule(plant_segment, simulation_config): + for module in plant_segment.post_bud_rule(plant_segment, simulation_config): + nproduce new(module[0], *module[1]) + + produce grow_object(plant_segment) # BUD PRODUCTION RULE # Controls bud break and branch initiation @@ -223,12 +231,11 @@ bud(bud_parameters) : if hasattr(new_branch, 'curve_x_range'): # Curved branches: set up custom growth guide curve - import time curve = create_bezier_curve(x_range=new_branch.curve_x_range, y_range=new_branch.curve_y_range, z_range=new_branch.curve_z_range, seed_value=time.time()) nproduce[@GcSetGuide(curve, new_branch.growth.max_length) else: # Straight branches: use default orientation - nproduce [ + nproduce [@Gc # Produce new branch with random orientation and growth object nproduce @RGetPos(new_branch.location.start)WoodStart(ParameterSet(type = new_branch))/(rd.random()*360)&(rd.random()*360)grow_object(new_branch)GetPos(new_branch.location.end)@Ge]bud(bud_parameters) diff --git a/examples/UFO/UFO_prototypes.py b/examples/UFO/UFO_prototypes.py index 0d67ffe..abcf9ba 100644 --- a/examples/UFO/UFO_prototypes.py +++ b/examples/UFO/UFO_prototypes.py @@ -5,7 +5,9 @@ import random as rd from dataclasses import dataclass import copy +from openalea.lpy import newmodule from helper import * +from openalea.lpy import Lsystem, AxialTree, newmodule class Spur(TreeBranch): def __init__(self, config=None, copy_from=None, prototype_dict: dict = {}): super().__init__(config, copy_from, prototype_dict) @@ -17,6 +19,16 @@ def is_bud_break(self, num_buds_segment): def create_branch(self): return None + + def pre_bud_rule(self, plant_segment, simulation_config): + return None + + def post_bud_rule(self, plant_segment, simulation_config): + radius = plant_segment.growth.thickness * simulation_config.thickness_multiplier + # return L-Py module directly + # from openalea.lpy import newModule + return [('@O', [float(radius)])] + class TertiaryBranch(TreeBranch): @@ -36,6 +48,12 @@ def create_branch(self): else: new_ob = Spur(copy_from = self.prototype_dict['spur']) return new_ob + + def pre_bud_rule(self, plant_segment, simulation_config): + return None + + def post_bud_rule(self, plant_segment, simulation_config): + return None class Branch(TreeBranch): def __init__(self, config=None, copy_from=None, prototype_dict: dict = {}): @@ -57,6 +75,12 @@ def create_branch(self): except: return None return new_ob + + def pre_bud_rule(self, plant_segment, simulation_config): + return None + + def post_bud_rule(self, plant_segment, simulation_config): + return None class Trunk(TreeBranch): @@ -75,6 +99,12 @@ def is_bud_break(self, num_buds_segment): def create_branch(self): if rd.random() > 0.1: return Branch(copy_from = self.prototype_dict['branch']) + + def pre_bud_rule(self, plant_segment, simulation_config): + return None + + def post_bud_rule(self, plant_segment, simulation_config ): + return None diff --git a/examples/UFO/UFO_simulation.py b/examples/UFO/UFO_simulation.py index d6a86dc..256cc59 100644 --- a/examples/UFO/UFO_simulation.py +++ b/examples/UFO/UFO_simulation.py @@ -36,7 +36,7 @@ class UFOSimulationConfig: # Growth Parameters thickness_multiplier: float = 1.2 # Multiplier for internode thickness - bud_age_tolerance: float = 0.01 # Tolerance for age-based bud spacing + tolerance: float = 1e-6 # Tolerance for age-based bud spacing # Visualization Parameters attractor_point_width: int = 10 # Width of attractor points in visualization diff --git a/helper.py b/helper.py index 478d12f..72f65e0 100644 --- a/helper.py +++ b/helper.py @@ -232,4 +232,7 @@ def create_bezier_curve(num_control_points=6, x_range=(-2, 2), y_range=(-2, 2), return BezierCurve(control_point_array) - \ No newline at end of file +def should_bud(plant_segment, simulation_config): + """Determine if a plant segment should produce a bud""" + return np.isclose(plant_segment.info.age % plant_segment.bud_spacing_age, 0, + atol=simulation_config.tolerance) diff --git a/stochastic_tree.py b/stochastic_tree.py index 204e968..d0a8113 100644 --- a/stochastic_tree.py +++ b/stochastic_tree.py @@ -176,6 +176,16 @@ def is_bud_break(self) -> bool: # if prob_break > self.bud_break_prob: # return True # return False + + @abstractmethod + def pre_bud_rule(self) -> str: + """This method can define any internal changes happening to the properties of the class, such as reduction in thickness increment etc.""" + pass + + @abstractmethod + def post_bud_rule(self) -> str: + """This method can define any internal changes happening to the properties of the class, such as reduction in thickness increment etc.""" + pass @abstractmethod def grow(self) -> None: @@ -378,6 +388,8 @@ def _get_parallel_and_perpendicular_components(self, vec_a, vec_b): return parallel_component, perpendicular_component + + class TreeBranch(BasicWood): """Base class for all tree branch types with common initialization logic""" From 1818d4dee38206a71eb2ad081c7a05a8095c4355 Mon Sep 17 00:00:00 2001 From: Abhinav Jain Date: Wed, 12 Nov 2025 15:46:39 -0800 Subject: [PATCH 09/14] Add example Envy --- examples/Envy/Envy.lpy | 266 ++++++++++++++++ examples/Envy/Envy_prototypes.py | 166 ++++++++++ examples/Envy/Envy_simulation.py | 294 ++++++++++++++++++ examples/UFO/UFO_simulation.py | 3 +- .../legacy/Camp_Envy_tie_prune_label.lpy | 0 .../legacy/Envy_tie_prune_label.lpy | 0 .../legacy/UFO_tie_prune_label.lpy | 0 .../legacy/static_envy.lpy | 0 .../legacy/static_ufo.lpy | 0 stochastic_tree.py | 4 +- 10 files changed, 731 insertions(+), 2 deletions(-) create mode 100644 examples/Envy/Envy.lpy create mode 100644 examples/Envy/Envy_prototypes.py create mode 100644 examples/Envy/Envy_simulation.py rename {examples => other_files}/legacy/Camp_Envy_tie_prune_label.lpy (100%) rename {examples => other_files}/legacy/Envy_tie_prune_label.lpy (100%) rename {examples => other_files}/legacy/UFO_tie_prune_label.lpy (100%) rename {examples => other_files}/legacy/static_envy.lpy (100%) rename {examples => other_files}/legacy/static_ufo.lpy (100%) diff --git a/examples/Envy/Envy.lpy b/examples/Envy/Envy.lpy new file mode 100644 index 0000000..d64a854 --- /dev/null +++ b/examples/Envy/Envy.lpy @@ -0,0 +1,266 @@ +""" +Tying, Pruning and labelling Envy architecture trees +""" +import sys +sys.path.append('../../') +from stochastic_tree import Support, BasicWood, TreeBranch, BasicWoodConfig +import time +from helper import * +import numpy as np +import random as rd +from examples.Envy.Envy_prototypes import basicwood_prototypes, Trunk +from examples.Envy.Envy_simulation import EnvySimulationConfig, generate_points_v_trellis, get_energy_mat, decide_guide, tie, prune +from dataclasses import dataclass +import time + +#init +main_trunk = Trunk(copy_from = basicwood_prototypes['trunk']) + +# Create simulation configuration +simulation_config = EnvySimulationConfig() + + + +trellis_support = Support(generate_points_v_trellis(simulation_config), simulation_config.support_num_wires, simulation_config.support_spacing_wires, simulation_config.support_trunk_wire_point) +tying_interval_iterations = simulation_config.num_iteration_tie +pruning_interval_iterations = simulation_config.num_iteration_prune +main_trunk.tying.guide_target = trellis_support.trunk_wire + +def StartEach(lstring): + """ + Initialize tying updates for trunk and branches at the start of each iteration. + + This function is called at the beginning of each L-System derivation iteration + to prepare the tree structure for potential tying operations. It ensures that + both the trunk and all child branches are ready to participate in the tying + process by updating their tying status. + + The function performs two main tasks: + 1. Updates the trunk's tying status if it has a wire target and hasn't been updated + 2. Updates all child branches' tying status if they haven't been updated + + Args: + lstring: The current L-System string (not used in this function but required + by L-Py's callback interface) + + Returns: + None: Modifies global tree structures in-place + + Note: + This function relies on global variables: branch_hierarchy, trellis_support, main_trunk. + It should be called automatically by L-Py at the start of each iteration. + """ + global branch_hierarchy, trellis_support, main_trunk + + # Update trunk tying status if it has a wire target and needs updating + if trellis_support.trunk_wire and not main_trunk.tying.tie_updated: + main_trunk.tie_update() + + # Update tying status for all child branches that need it + for branch in branch_hierarchy[main_trunk.name]: + if not branch.tying.tie_updated: + branch.tie_update() + + +def EndEach(lstring): + """ + Perform tying and pruning operations at the end of each L-System iteration. + + This function is the main orchestration point for the tree training simulation. + It executes tying operations (assigning branches to trellis wires) and pruning + operations based on configurable iteration intervals. The tying process uses + energy-based optimization to find optimal branch-to-wire assignments. + + The function performs the following sequence when tying is scheduled: + 1. Updates the trunk's guide target (ties trunk first) + 2. Calculates energy matrix for all branch-wire combinations + 3. Uses greedy optimization to assign branches to lowest-energy wires + 4. Updates guide targets for all assigned branches + 5. Performs actual tying operations in the L-System string + 6. Prunes old branches if pruning iteration is reached + + Args: + lstring: The current L-System string containing modules and their parameters + + Returns: + The modified L-System string after tying and pruning operations + + Note: + This function relies on global variables and is called automatically by L-Py + at the end of each derivation iteration. Tying occurs every tying_interval_iterations + iterations, while pruning occurs every pruning_interval_iterations iterations. + """ + global branch_hierarchy, trellis_support, tying_interval_iterations + + current_iteration = getIterationNb() + 1 #GetIterationNb is an L-Py function + + # Check if this is a tying iteration + if current_iteration % tying_interval_iterations == 0: + # Tie trunk to its target wire first (one iteration before branches) + if trellis_support.trunk_wire: + main_trunk.update_guide(main_trunk.tying.guide_target) + + # Calculate energy costs for optimal branch-to-wire assignments + branches = branch_hierarchy[main_trunk.name] + energy_matrix = get_energy_mat(branches, trellis_support, simulation_config) + + # Perform greedy optimization to assign branches to wires + decide_guide(energy_matrix, branches, trellis_support, simulation_config) + + # Update guide targets for all assigned branches + for branch in branches: + branch.update_guide(branch.tying.guide_target) + + # Execute tying operations in the L-System string + while tie(lstring, simulation_config): + pass # Continue until no more tying operations are possible + + # Check if this is also a pruning iteration + if current_iteration % pruning_interval_iterations == 0: + # Prune branches until no more can be pruned + while prune(lstring, simulation_config): + pass + + return lstring + + + +branch_hierarchy = {} +branch_hierarchy[main_trunk.name] = [] +enable_color_labeling = simulation_config.label +# ============================================================================= +# L-SYSTEM GRAMMAR DEFINITION +# ============================================================================= +# This section defines the formal grammar for the Envy tree growth simulation. +# The L-System uses modules (symbols) to represent different plant components +# and their growth behaviors. + +# ----------------------------------------------------------------------------- +# MODULE DECLARATIONS +# ----------------------------------------------------------------------------- +# Define the vocabulary of symbols used in the L-System grammar. +# Each module represents a different type of plant component or operation. + +module Attractors # Trellis support structure that guides branch growth +module grow_object # Growing plant parts (trunk, branches, spurs) with length/thickness +module bud # Dormant buds that can break to produce new branches +module branch # Branch segments in the L-System string +module WoodStart # Starting point of wood segments (used for tying operations) + +# ----------------------------------------------------------------------------- +# GLOBAL L-SYSTEM PARAMETERS +# ----------------------------------------------------------------------------- +# Create a growth guide curve for the initial trunk development +trunk_guide_curve = create_bezier_curve(x_range = (-1, 1), y_range = (-1, 1), z_range = (0, 10), seed_value=time.time()) + +# ----------------------------------------------------------------------------- +# AXIOM (STARTING STRING) +# ----------------------------------------------------------------------------- +# The initial L-System string that begins the simulation. +# Starts with the trellis attractors, sets up the trunk guide curve, +# and initializes the trunk growth with proper orientation. +Axiom: Attractors(trellis_support)SetGuide(trunk_guide_curve, main_trunk.growth.max_length)[@GcGetPos(main_trunk.location.start)WoodStart(ParameterSet(type = main_trunk))grow_object(main_trunk)GetPos(main_trunk.location.end)@Ge] + +# ----------------------------------------------------------------------------- +# DERIVATION PARAMETERS +# ----------------------------------------------------------------------------- +# Set the maximum number of derivation steps for the L-System +derivation length: simulation_config.derivation_length + +# ----------------------------------------------------------------------------- +# PRODUCTION RULES +# ----------------------------------------------------------------------------- +# Define how each module type evolves during each derivation step. +# These rules control the growth, branching, and development of the tree. + +production: + +# GROW_OBJECT PRODUCTION RULE +# Handles the growth of plant segments (trunk, branches, spurs) +# Determines whether to continue growing, stop, or produce buds +grow_object(plant_segment) : + if plant_segment == None: + # Null object - terminate this branch + produce * + if plant_segment.length >= plant_segment.growth.max_length: + # Maximum length reached - stop growing this segment + nproduce * + else: + # Continue growing - update segment properties + nproduce SetContour(plant_segment.contour) + #Update internal state of the plant segment + plant_segment.grow_one() + #Update physical representation + if enable_color_labeling: + # Add color visualization if labeling is enabled -- Can this be moved to the start of building the segment? + r, g, b = plant_segment.info.color + nproduce SetColor(r,g,b) + #Produce internode segment + nproduce I(plant_segment.growth.growth_length, plant_segment.growth.thickness, plant_segment) + #Produce bud + if plant_segment.pre_bud_rule(plant_segment, simulation_config): + for module in plant_segment.post_bud_rule(plant_segment, simulation_config): + nproduce new(module[0], *module[1]) + + if should_bud(plant_segment, simulation_config): + # Age-based bud production for lateral branching + nproduce bud(ParameterSet(type = plant_segment, num_buds = 0)) + + if plant_segment.post_bud_rule(plant_segment, simulation_config): + for module in plant_segment.post_bud_rule(plant_segment, simulation_config): + nproduce new(module[0], *module[1]) + + produce grow_object(plant_segment) + +# BUD PRODUCTION RULE +# Controls bud break and branch initiation +# Determines when buds activate to produce new branches +bud(bud_parameters) : + if bud_parameters.type.is_bud_break(bud_parameters.num_buds): + # Bud break condition met - create new branch + + new_branch = bud_parameters.type.create_branch() + if new_branch == None: + # Branch creation failed - terminate + produce * + # Register new branch in parent-child relationship tracking + branch_hierarchy[new_branch.name] = [] + branch_hierarchy[bud_parameters.type.name].append(new_branch) + # Update branch counters + bud_parameters.num_buds+=1 + bud_parameters.type.info.num_branches+=1 + + if hasattr(new_branch, 'curve_x_range'): + # Curved branches: set up custom growth guide curve + curve = create_bezier_curve(x_range=new_branch.curve_x_range, y_range=new_branch.curve_y_range, z_range=new_branch.curve_z_range, seed_value=time.time()) + nproduce[@GcSetGuide(curve, new_branch.growth.max_length) + else: + # Straight branches: use default orientation + nproduce [@Gc + # Produce new branch with random orientation and growth object + nproduce @RGetPos(new_branch.location.start)WoodStart(ParameterSet(type = new_branch))/(rd.random()*360)&(rd.random()*360)grow_object(new_branch)GetPos(new_branch.location.end)@Ge]bud(bud_parameters) + + +# ----------------------------------------------------------------------------- +# GEOMETRIC INTERPRETATION (HOMOMORPHISM) +# ----------------------------------------------------------------------------- +# Map abstract L-System modules to concrete 3D geometry for rendering. +# These rules define how the symbolic representation becomes visual. + +homomorphism: + +# Internode segments become cylinders with length and radius +I(a,r,o) --> F(a,r) +# Branch segments also become cylinders +S(a,r,o) --> F(a,r) + +# ----------------------------------------------------------------------------- +# ATTRACTOR VISUALIZATION +# ----------------------------------------------------------------------------- +# Additional production rules for displaying trellis attractor points +production: +Attractors(trellis_support): + # Display enabled attractor points as visual markers + points_to_display = trellis_support.attractor_grid.get_enabled_points() + if len(points_to_display) > 0: + produce [,(3) @g(PointSet(points_to_display,width=simulation_config.attractor_point_width))] diff --git a/examples/Envy/Envy_prototypes.py b/examples/Envy/Envy_prototypes.py new file mode 100644 index 0000000..fdc8488 --- /dev/null +++ b/examples/Envy/Envy_prototypes.py @@ -0,0 +1,166 @@ +import sys +sys.path.append('../../') +from stochastic_tree import Support, BasicWood, TreeBranch, BasicWoodConfig +import numpy as np +import random as rd +from dataclasses import dataclass +import copy +from openalea.lpy import newmodule +from helper import * + + +class Spur(TreeBranch): + def __init__(self, config=None, copy_from=None, prototype_dict: dict = {}): + super().__init__(config, copy_from, prototype_dict) + + def is_bud_break(self, num_buds_segment): + return (rd.random() < 0.1) + + def create_branch(self): + return None + + def pre_bud_rule(self, plant_segment, simulation_config): + return None + + def post_bud_rule(self, plant_segment, simulation_config): + return None + + +class Branch(TreeBranch): + def __init__(self, config=None, copy_from=None, prototype_dict: dict = {}): + super().__init__(config, copy_from, prototype_dict) + self.num_buds_segment = 0 + + def is_bud_break(self, num_break_buds): + if num_break_buds >= 1: + return False + return (rd.random() < 0.5*(1 - self.num_buds_segment/self.growth.max_buds_segment)) + + def create_branch(self): + self.num_buds_segment += 1 + if rd.random() > 0.8: + new_ob = NonTrunk(copy_from=self.prototype_dict['nontrunk']) + else: + new_ob = Spur(copy_from=self.prototype_dict['spur']) + return new_ob + + def pre_bud_rule(self, plant_segment, simulation_config): + return None + + def post_bud_rule(self, plant_segment, simulation_config): + return None + + +class Trunk(TreeBranch): + """ Details of the trunk while growing a tree, length, thickness, where to attach them etc """ + def __init__(self, config=None, copy_from=None, prototype_dict: dict = {}): + super().__init__(config, copy_from, prototype_dict) + + def is_bud_break(self, num_buds_segment): + if (rd.random() > 0.1*(1 - num_buds_segment/self.growth.max_buds_segment)): + return False + return True + + def create_branch(self): + if rd.random() > 0.8: + return Spur(copy_from=self.prototype_dict['spur']) + else: + return Branch(copy_from=self.prototype_dict['branch']) + + def pre_bud_rule(self, plant_segment, simulation_config): + return None + + def post_bud_rule(self, plant_segment, simulation_config): + return None + + +class NonTrunk(TreeBranch): + def __init__(self, config=None, copy_from=None, prototype_dict: dict = {}): + super().__init__(config, copy_from, prototype_dict) + + def is_bud_break(self, num_buds_segment): + return (rd.random() < 0.5*(1 - num_buds_segment/self.growth.max_buds_segment)) + + def create_branch(self): + if rd.random() > 0.3: + return None + else: + new_ob = Spur(copy_from=self.prototype_dict['spur']) + return new_ob + + def pre_bud_rule(self, plant_segment, simulation_config): + return None + + def post_bud_rule(self, plant_segment, simulation_config): + return None + + +# growth_length = 0.1 +basicwood_prototypes = {} + +# Create configs for cleaner prototype setup +spur_config = BasicWoodConfig( + max_buds_segment=5, + tie_axis=None, + max_length=0.2, + thickness=0.003, + growth_length=0.05, # growth_length/2 from original + thickness_increment=0., + color=[0, 255, 0], + bud_spacing_age=2, + curve_x_range=(-0.2, 0.2), + curve_y_range=(-0.2, 0.2), + curve_z_range=(-1, 1), + prunable=True +) + +branch_config = BasicWoodConfig( + max_buds_segment=30, + tie_axis=(1, 0, 0), + max_length=2.2, + thickness=0.01, + growth_length=0.1, + thickness_increment=0.00001, + color=[255, 150, 0], + bud_spacing_age=2, + curve_x_range=(-0.5, 0.5), + curve_y_range=(-0.5, 0.5), + curve_z_range=(-1, 1), + prunable=True +) + +trunk_config = BasicWoodConfig( + max_buds_segment=5, + tie_axis=None, + max_length=4, + thickness=0.01, + growth_length=0.1, + thickness_increment=0.00001, + color=[255, 0, 0], + bud_spacing_age=2, + curve_x_range=(-1, 1), + curve_y_range=(-0.15, 0.15), + curve_z_range=(0, 10), + prunable=False +) + +nontrunk_config = BasicWoodConfig( + max_buds_segment=5, + tie_axis=None, + max_length=0.3, + thickness=0.003, + growth_length=0.05, # growth_length/2 from original + thickness_increment=0.00001, + color=[0, 255, 0], + bud_spacing_age=2, + curve_x_range=(-0.5, 0.5), + curve_y_range=(-0.5, 0.5), + curve_z_range=(-1, 1), + prunable=True +) + +# Setup prototypes using configs +basicwood_prototypes['spur'] = Spur(config=spur_config, prototype_dict=basicwood_prototypes) +basicwood_prototypes['branch'] = Branch(config=branch_config, prototype_dict=basicwood_prototypes) +basicwood_prototypes['trunk'] = Trunk(config=trunk_config, prototype_dict=basicwood_prototypes) +basicwood_prototypes['nontrunk'] = NonTrunk(config=nontrunk_config, prototype_dict=basicwood_prototypes) \ No newline at end of file diff --git a/examples/Envy/Envy_simulation.py b/examples/Envy/Envy_simulation.py new file mode 100644 index 0000000..6ac20c2 --- /dev/null +++ b/examples/Envy/Envy_simulation.py @@ -0,0 +1,294 @@ +from dataclasses import dataclass +import numpy as np +from helper import cut_from + +@dataclass +class EnvySimulationConfig: + """Configuration for Envy trellis tree simulation parameters.""" + + # Tying and Pruning + num_iteration_tie: int = 5 + num_iteration_prune: int = 16 + + # Display + label: bool = True + + # Support Structure + support_trunk_wire_point = None + support_num_wires: int = 14 + support_spacing_wires: int = 1 + + # Point Generation + trellis_x_value: float = 0.45 + trellis_z_start: float = 0.6 + trellis_z_end: float = 3.4 + trellis_z_spacing: float = 0.45 + + # Energy and Tying Parameters + energy_threshold: float = 0.25 + energy_decay_factor: float = 105 + tolerance: float = 1e-5 + # Energy and Tying Parameters + energy_distance_weight: float = 0.5 # Weight for distance in energy calculation (was hardcoded /2) + energy_threshold: float = 1.0 # Maximum energy threshold for tying + + + # Pruning Parameters + pruning_age_threshold: int = 6 + + # L-System Parameters + derivation_length: int = 128 + + # Growth Parameters + growth_length: float = 0.1 + bud_spacing_age: int = 2 + + # Visualization Parameters + attractor_point_width: int = 10 + + +def generate_points_v_trellis(simulation_config): + """ + Generate 3D points for the V-trellis wire structure. + + Creates a linear array of wire attachment points along the x-axis at a fixed + height (z) and depth (y). The points are spaced evenly within the configured + z-range and used to construct the trellis support structure. + """ + x = np.full((7,), simulation_config.trellis_x_value).astype(float) + y = np.full((7,), 0).astype(float) + z = np.arange(simulation_config.trellis_z_start, + simulation_config.trellis_z_end, + simulation_config.trellis_z_spacing) + + pts = [] + for i in range(x.shape[0]): + pts.append((-x[i], y[i], z[i])) + pts.append((x[i], y[i], z[i])) + return pts + + + +def get_energy_mat(branches, arch, simulation_config): + """ + Calculate the energy matrix for optimal branch-to-wire assignment. + + This function computes an energy cost matrix where each entry represents the + "cost" of assigning a specific branch to a specific wire in the trellis system. + The energy is based on the Euclidean distance from wire attachment points to + both the start and end points of each branch, weighted by the simulation's + distance weight parameter. + + The algorithm uses a greedy optimization approach where branches are assigned + to the lowest-energy available wire that hasn't reached capacity. + + Args: + branches: List of branch objects to be assigned to wires + arch: Support architecture object containing wire information + + Returns: + numpy.ndarray: Energy matrix of shape (num_branches, num_wires) where + matrix[i][j] is the energy cost of assigning branch i to wire j. + Untied branches and occupied wires have infinite energy (np.inf). + """ + num_branches = len(branches) + num_wires = len(arch.branch_supports) + + # Initialize energy matrix with infinite values (impossible assignments) + energy_matrix = np.full((num_branches, num_wires), np.inf) + + # Calculate energy costs for all valid branch-wire combinations + for branch_idx, branch in enumerate(branches): + # Skip branches that are already tied + if branch.tying.has_tied: + continue + + for wire_id, wire in arch.branch_supports.items(): + # Skip wires that already have a branch attached + if wire.num_branch >= 1: + continue + + # Calculate weighted distance energy for this branch-wire pair + # Energy considers distance from wire to both branch endpoints + wire_point = np.array(wire.point) + branch_start = np.array(branch.location.start) + branch_end = np.array(branch.location.end) + + start_distance_energy = np.sum((wire_point - branch_start) ** 2) + end_distance_energy = np.sum((wire_point - branch_end) ** 2) + + total_energy = (start_distance_energy + end_distance_energy) * simulation_config.energy_distance_weight + + energy_matrix[branch_idx, wire_id] = total_energy + + return energy_matrix + +def decide_guide(energy_matrix, branches, arch, simulation_config): + """ + Perform greedy assignment of branches to wires based on energy matrix. + + This function implements a greedy optimization algorithm that iteratively assigns + the branch-wire pair with the lowest energy cost. Once a branch is assigned to + a wire, both that branch and wire are marked as unavailable (infinite energy) + to prevent further assignments. + + The algorithm continues until no valid assignments remain (all remaining energies + are infinite or above the threshold). + + Args: + energy_matrix: numpy.ndarray of shape (num_branches, num_wires) with energy costs + branches: List of branch objects to be assigned + arch: Support architecture containing wire information + + Returns: + None: Modifies branches and arch in-place with new assignments + """ + num_branches, num_wires = energy_matrix.shape + + # Early return if no branches or wires to assign + if num_branches == 0 or num_wires == 0: + return + + # Continue making assignments until no valid ones remain + while True: + # Find the minimum energy value and its position + min_energy_indices = np.argwhere(energy_matrix == np.min(energy_matrix)) + + # If no valid indices found or matrix is empty, stop + if len(min_energy_indices) == 0: + break + + # Get the first (and typically only) minimum energy position + branch_idx, wire_id = min_energy_indices[0] + min_energy = energy_matrix[branch_idx, wire_id] + + # Stop if minimum energy is infinite (no valid assignments) or above threshold + if np.isinf(min_energy) or min_energy > simulation_config.energy_threshold: + break + + # Get the branch and wire objects + branch = branches[branch_idx] + wire = arch.branch_supports[wire_id] + + # Skip if branch is already tied (defensive check) + if branch.tying.has_tied: + # Mark this assignment as invalid and continue + energy_matrix[branch_idx, wire_id] = np.inf + continue + + # Perform the assignment + branch.tying.guide_target = wire + wire.add_branch() + + # Mark branch and wire as unavailable for future assignments + # Set entire row (branch) to infinity - this branch can't be assigned again + energy_matrix[branch_idx, :] = np.inf + # Set entire column (wire) to infinity - this wire can't accept more branches + energy_matrix[:, wire_id] = np.inf + + +def prune(lstring, simulation_config): + """ + Prune old branches that exceed the age threshold and haven't been tied to wires. + + This function implements the pruning strategy for the tree training simulation. + It identifies branches that have grown too old (exceeding the pruning age threshold) + but haven't been successfully tied to trellis wires. Such branches are considered + unproductive and are removed from the L-System to encourage new growth. + + The pruning criteria are: + 1. Branch age exceeds the configured pruning threshold + 2. Branch has not been tied to any trellis wire + 3. Branch has not already been marked for cutting + + When a branch meets all criteria, it is: + - Marked as cut (to prevent re-processing) + - Removed from the L-System string using cut_from() + + Args: + lstring: The current L-System string containing modules and their parameters + + Returns: + bool: True if a branch was pruned, False if no eligible branches found + + Note: + This function processes one branch at a time and returns immediately after + pruning a single branch. It should be called repeatedly (e.g., in a while loop) + until no more pruning operations are possible. The cut_from() function handles + the actual removal of the branch and any dependent substructures from the string. + """ + for position, symbol in enumerate(lstring): + # Check if this is a WoodStart module (represents a branch) + if symbol.name == 'WoodStart': + branch = symbol[0].type + + # Check pruning criteria + age_exceeds_threshold = branch.info.age > simulation_config.pruning_age_threshold + not_tied_to_wire = not branch.tying.has_tied + not_already_cut = not branch.info.cut + is_prunable = branch.info.prunable + + # Prune if all criteria are met + if age_exceeds_threshold and not_tied_to_wire and not_already_cut and is_prunable: + # Mark branch as cut to prevent re-processing + branch.info.cut = True + + # Remove the branch from the L-System string + print("Pruning branch at position:", position, lstring[position]) # Debug statement + lstring = cut_from(position, lstring) + + return True + + return False + +def tie(lstring, simulation_config): + """ + Perform tying operation on eligible branches in the L-System string. + + This function searches through the L-System string for 'WoodStart' modules that + represent branches ready for tying to trellis wires. It identifies branches that: + 1. Have tying properties (tying attribute exists) + 2. Have a defined tie axis (tie_axis is not None) + 3. Have not been tied yet (tie_updated is False) + 4. Have guide points available for wire attachment + + When an eligible branch is found, it performs the tying operation by: + - Marking the branch as tied (tie_updated = False) + - Adding the branch to the target wire + - Calling the branch's tie_lstring method to modify the L-System string + + Args: + lstring: The current L-System string containing modules and their parameters + + Returns: + bool: True if a tying operation was performed, False if no eligible branches found + + Note: + This function processes one branch at a time and returns immediately after + tying a single branch. It should be called repeatedly (e.g., in a while loop) + until no more tying operations are possible. + """ + for position, symbol in enumerate(lstring): + # Check if this is a WoodStart module with tying capabilities + if (symbol == 'WoodStart' and + hasattr(symbol[0].type, 'tying') and + getattr(symbol[0].type.tying, 'tie_axis', None) is not None): + + branch = symbol[0].type + + # Skip branches that have already been processed for tying + if not branch.tying.tie_updated: + continue + + # Check if branch has guide points for wire attachment + if branch.tying.guide_points: + # Perform the tying operation + branch.tying.tie_updated = False + branch.tying.guide_target.add_branch() + + # Update the L-System string with tying modifications + lstring, modifications_count = branch.tie_lstring(lstring, position) + + return True + + return False \ No newline at end of file diff --git a/examples/UFO/UFO_simulation.py b/examples/UFO/UFO_simulation.py index 256cc59..5323c95 100644 --- a/examples/UFO/UFO_simulation.py +++ b/examples/UFO/UFO_simulation.py @@ -232,9 +232,10 @@ def prune(lstring, simulation_config): age_exceeds_threshold = branch.info.age > simulation_config.pruning_age_threshold not_tied_to_wire = not branch.tying.has_tied not_already_cut = not branch.info.cut + is_prunable = branch.info.prunable # Prune if all criteria are met - if age_exceeds_threshold and not_tied_to_wire and not_already_cut: + if age_exceeds_threshold and not_tied_to_wire and not_already_cut and is_prunable: # Mark branch as cut to prevent re-processing branch.info.cut = True diff --git a/examples/legacy/Camp_Envy_tie_prune_label.lpy b/other_files/legacy/Camp_Envy_tie_prune_label.lpy similarity index 100% rename from examples/legacy/Camp_Envy_tie_prune_label.lpy rename to other_files/legacy/Camp_Envy_tie_prune_label.lpy diff --git a/examples/legacy/Envy_tie_prune_label.lpy b/other_files/legacy/Envy_tie_prune_label.lpy similarity index 100% rename from examples/legacy/Envy_tie_prune_label.lpy rename to other_files/legacy/Envy_tie_prune_label.lpy diff --git a/examples/legacy/UFO_tie_prune_label.lpy b/other_files/legacy/UFO_tie_prune_label.lpy similarity index 100% rename from examples/legacy/UFO_tie_prune_label.lpy rename to other_files/legacy/UFO_tie_prune_label.lpy diff --git a/examples/legacy/static_envy.lpy b/other_files/legacy/static_envy.lpy similarity index 100% rename from examples/legacy/static_envy.lpy rename to other_files/legacy/static_envy.lpy diff --git a/examples/legacy/static_ufo.lpy b/other_files/legacy/static_ufo.lpy similarity index 100% rename from examples/legacy/static_ufo.lpy rename to other_files/legacy/static_ufo.lpy diff --git a/stochastic_tree.py b/stochastic_tree.py index d0a8113..f3d2f14 100644 --- a/stochastic_tree.py +++ b/stochastic_tree.py @@ -84,6 +84,7 @@ class BasicWoodConfig: order: int = 0 color: int = 0 material: int = 0 + prunable: bool = True name: str = None bud_spacing_age: int = 2 # Age interval for bud creation @@ -123,6 +124,7 @@ def __init__(self, config=None, copy_from=None, **kwargs): order = config.order color = config.color material = config.material + prunable = config.prunable name = config.name bud_spacing_age = config.bud_spacing_age curve_x_range = config.curve_x_range @@ -140,7 +142,7 @@ def __init__(self, config=None, copy_from=None, **kwargs): self.tying = TyingState(tie_axis=tie_axis) self.current_tied = False #Information Variables - self.info = InfoState(order=order, color=color, material=material) + self.info = InfoState(order=order, color=color, material=material, prunable=prunable) self.__length = 0 #Growth Variables self.growth = GrowthState( From bfee2ceddac7bf83724c7059a8f7d65b4c842efb Mon Sep 17 00:00:00 2001 From: Abhinav Jain Date: Wed, 12 Nov 2025 17:18:05 -0800 Subject: [PATCH 10/14] Add simulation_base file and updated examples --- examples/Envy/Envy_simulation.py | 318 ++++++------------------------ examples/UFO/UFO.lpy | 14 +- examples/UFO/UFO_prototypes.py | 5 +- examples/UFO/UFO_simulation.py | 320 ++++++------------------------- simulation_base.py | 315 ++++++++++++++++++++++++++++++ 5 files changed, 438 insertions(+), 534 deletions(-) create mode 100644 simulation_base.py diff --git a/examples/Envy/Envy_simulation.py b/examples/Envy/Envy_simulation.py index 6ac20c2..03424fa 100644 --- a/examples/Envy/Envy_simulation.py +++ b/examples/Envy/Envy_simulation.py @@ -1,294 +1,90 @@ +import sys +sys.path.append('../../') from dataclasses import dataclass import numpy as np -from helper import cut_from +from simulation_base import SimulationConfig, TreeSimulationBase @dataclass -class EnvySimulationConfig: +class EnvySimulationConfig(SimulationConfig): """Configuration for Envy trellis tree simulation parameters.""" - # Tying and Pruning + # Override base defaults for Envy-specific values num_iteration_tie: int = 5 num_iteration_prune: int = 16 + pruning_age_threshold: int = 6 + derivation_length: int = 128 - # Display - label: bool = True - - # Support Structure + # Envy-specific Support Structure support_trunk_wire_point = None support_num_wires: int = 14 - support_spacing_wires: int = 1 - # Point Generation + # Envy-specific Point Generation (V-trellis) trellis_x_value: float = 0.45 trellis_z_start: float = 0.6 trellis_z_end: float = 3.4 trellis_z_spacing: float = 0.45 - # Energy and Tying Parameters - energy_threshold: float = 0.25 - energy_decay_factor: float = 105 - tolerance: float = 1e-5 - # Energy and Tying Parameters - energy_distance_weight: float = 0.5 # Weight for distance in energy calculation (was hardcoded /2) - energy_threshold: float = 1.0 # Maximum energy threshold for tying - - - # Pruning Parameters - pruning_age_threshold: int = 6 - - # L-System Parameters - derivation_length: int = 128 - - # Growth Parameters + # Envy-specific Growth Parameters growth_length: float = 0.1 bud_spacing_age: int = 2 - # Visualization Parameters - attractor_point_width: int = 10 - - -def generate_points_v_trellis(simulation_config): - """ - Generate 3D points for the V-trellis wire structure. - Creates a linear array of wire attachment points along the x-axis at a fixed - height (z) and depth (y). The points are spaced evenly within the configured - z-range and used to construct the trellis support structure. +class EnvySimulation(TreeSimulationBase): """ - x = np.full((7,), simulation_config.trellis_x_value).astype(float) - y = np.full((7,), 0).astype(float) - z = np.arange(simulation_config.trellis_z_start, - simulation_config.trellis_z_end, - simulation_config.trellis_z_spacing) - - pts = [] - for i in range(x.shape[0]): - pts.append((-x[i], y[i], z[i])) - pts.append((x[i], y[i], z[i])) - return pts - - - -def get_energy_mat(branches, arch, simulation_config): - """ - Calculate the energy matrix for optimal branch-to-wire assignment. - - This function computes an energy cost matrix where each entry represents the - "cost" of assigning a specific branch to a specific wire in the trellis system. - The energy is based on the Euclidean distance from wire attachment points to - both the start and end points of each branch, weighted by the simulation's - distance weight parameter. + Envy trellis architecture simulation. - The algorithm uses a greedy optimization approach where branches are assigned - to the lowest-energy available wire that hasn't reached capacity. - - Args: - branches: List of branch objects to be assigned to wires - arch: Support architecture object containing wire information - - Returns: - numpy.ndarray: Energy matrix of shape (num_branches, num_wires) where - matrix[i][j] is the energy cost of assigning branch i to wire j. - Untied branches and occupied wires have infinite energy (np.inf). + Implements the Envy V-trellis training system with wires arranged in a V-shape + on both sides of the tree row. """ - num_branches = len(branches) - num_wires = len(arch.branch_supports) - - # Initialize energy matrix with infinite values (impossible assignments) - energy_matrix = np.full((num_branches, num_wires), np.inf) - - # Calculate energy costs for all valid branch-wire combinations - for branch_idx, branch in enumerate(branches): - # Skip branches that are already tied - if branch.tying.has_tied: - continue - - for wire_id, wire in arch.branch_supports.items(): - # Skip wires that already have a branch attached - if wire.num_branch >= 1: - continue - - # Calculate weighted distance energy for this branch-wire pair - # Energy considers distance from wire to both branch endpoints - wire_point = np.array(wire.point) - branch_start = np.array(branch.location.start) - branch_end = np.array(branch.location.end) - - start_distance_energy = np.sum((wire_point - branch_start) ** 2) - end_distance_energy = np.sum((wire_point - branch_end) ** 2) - - total_energy = (start_distance_energy + end_distance_energy) * simulation_config.energy_distance_weight - - energy_matrix[branch_idx, wire_id] = total_energy - return energy_matrix + def generate_points(self): + """ + Generate 3D points for the V-trellis wire structure. -def decide_guide(energy_matrix, branches, arch, simulation_config): - """ - Perform greedy assignment of branches to wires based on energy matrix. - - This function implements a greedy optimization algorithm that iteratively assigns - the branch-wire pair with the lowest energy cost. Once a branch is assigned to - a wire, both that branch and wire are marked as unavailable (infinite energy) - to prevent further assignments. - - The algorithm continues until no valid assignments remain (all remaining energies - are infinite or above the threshold). - - Args: - energy_matrix: numpy.ndarray of shape (num_branches, num_wires) with energy costs - branches: List of branch objects to be assigned - arch: Support architecture containing wire information + Creates a linear array of wire attachment points along the x-axis at a fixed + height (z) and depth (y). The points are spaced evenly within the configured + z-range and used to construct the trellis support structure. - Returns: - None: Modifies branches and arch in-place with new assignments - """ - num_branches, num_wires = energy_matrix.shape - - # Early return if no branches or wires to assign - if num_branches == 0 or num_wires == 0: - return - - # Continue making assignments until no valid ones remain - while True: - # Find the minimum energy value and its position - min_energy_indices = np.argwhere(energy_matrix == np.min(energy_matrix)) - - # If no valid indices found or matrix is empty, stop - if len(min_energy_indices) == 0: - break - - # Get the first (and typically only) minimum energy position - branch_idx, wire_id = min_energy_indices[0] - min_energy = energy_matrix[branch_idx, wire_id] - - # Stop if minimum energy is infinite (no valid assignments) or above threshold - if np.isinf(min_energy) or min_energy > simulation_config.energy_threshold: - break - - # Get the branch and wire objects - branch = branches[branch_idx] - wire = arch.branch_supports[wire_id] - - # Skip if branch is already tied (defensive check) - if branch.tying.has_tied: - # Mark this assignment as invalid and continue - energy_matrix[branch_idx, wire_id] = np.inf - continue - - # Perform the assignment - branch.tying.guide_target = wire - wire.add_branch() - - # Mark branch and wire as unavailable for future assignments - # Set entire row (branch) to infinity - this branch can't be assigned again - energy_matrix[branch_idx, :] = np.inf - # Set entire column (wire) to infinity - this wire can't accept more branches - energy_matrix[:, wire_id] = np.inf - - -def prune(lstring, simulation_config): - """ - Prune old branches that exceed the age threshold and haven't been tied to wires. - - This function implements the pruning strategy for the tree training simulation. - It identifies branches that have grown too old (exceeding the pruning age threshold) - but haven't been successfully tied to trellis wires. Such branches are considered - unproductive and are removed from the L-System to encourage new growth. - - The pruning criteria are: - 1. Branch age exceeds the configured pruning threshold - 2. Branch has not been tied to any trellis wire - 3. Branch has not already been marked for cutting - - When a branch meets all criteria, it is: - - Marked as cut (to prevent re-processing) - - Removed from the L-System string using cut_from() - - Args: - lstring: The current L-System string containing modules and their parameters - - Returns: - bool: True if a branch was pruned, False if no eligible branches found - - Note: - This function processes one branch at a time and returns immediately after - pruning a single branch. It should be called repeatedly (e.g., in a while loop) - until no more pruning operations are possible. The cut_from() function handles - the actual removal of the branch and any dependent substructures from the string. - """ - for position, symbol in enumerate(lstring): - # Check if this is a WoodStart module (represents a branch) - if symbol.name == 'WoodStart': - branch = symbol[0].type - - # Check pruning criteria - age_exceeds_threshold = branch.info.age > simulation_config.pruning_age_threshold - not_tied_to_wire = not branch.tying.has_tied - not_already_cut = not branch.info.cut - is_prunable = branch.info.prunable - - # Prune if all criteria are met - if age_exceeds_threshold and not_tied_to_wire and not_already_cut and is_prunable: - # Mark branch as cut to prevent re-processing - branch.info.cut = True + Returns: + list: List of (x, y, z) tuples representing wire attachment points in V-trellis formation + """ + x = np.full((7,), self.config.trellis_x_value).astype(float) + y = np.full((7,), 0).astype(float) + z = np.arange(self.config.trellis_z_start, + self.config.trellis_z_end, + self.config.trellis_z_spacing) + + pts = [] + for i in range(x.shape[0]): + pts.append((-x[i], y[i], z[i])) + pts.append((x[i], y[i], z[i])) + return pts + + +# Backwards compatibility: provide standalone functions that use the class +def generate_points_v_trellis(simulation_config): + """Backward compatibility wrapper for generate_points.""" + sim = EnvySimulation(simulation_config) + return sim.generate_points() - # Remove the branch from the L-System string - print("Pruning branch at position:", position, lstring[position]) # Debug statement - lstring = cut_from(position, lstring) +def get_energy_mat(branches, arch, simulation_config): + """Backward compatibility wrapper for get_energy_mat.""" + sim = EnvySimulation(simulation_config) + return sim.get_energy_mat(branches, arch) - return True +def decide_guide(energy_matrix, branches, arch, simulation_config): + """Backward compatibility wrapper for decide_guide.""" + sim = EnvySimulation(simulation_config) + return sim.decide_guide(energy_matrix, branches, arch) - return False +def prune(lstring, simulation_config): + """Backward compatibility wrapper for prune.""" + sim = EnvySimulation(simulation_config) + return sim.prune(lstring) def tie(lstring, simulation_config): - """ - Perform tying operation on eligible branches in the L-System string. - - This function searches through the L-System string for 'WoodStart' modules that - represent branches ready for tying to trellis wires. It identifies branches that: - 1. Have tying properties (tying attribute exists) - 2. Have a defined tie axis (tie_axis is not None) - 3. Have not been tied yet (tie_updated is False) - 4. Have guide points available for wire attachment - - When an eligible branch is found, it performs the tying operation by: - - Marking the branch as tied (tie_updated = False) - - Adding the branch to the target wire - - Calling the branch's tie_lstring method to modify the L-System string - - Args: - lstring: The current L-System string containing modules and their parameters - - Returns: - bool: True if a tying operation was performed, False if no eligible branches found - - Note: - This function processes one branch at a time and returns immediately after - tying a single branch. It should be called repeatedly (e.g., in a while loop) - until no more tying operations are possible. - """ - for position, symbol in enumerate(lstring): - # Check if this is a WoodStart module with tying capabilities - if (symbol == 'WoodStart' and - hasattr(symbol[0].type, 'tying') and - getattr(symbol[0].type.tying, 'tie_axis', None) is not None): - - branch = symbol[0].type - - # Skip branches that have already been processed for tying - if not branch.tying.tie_updated: - continue - - # Check if branch has guide points for wire attachment - if branch.tying.guide_points: - # Perform the tying operation - branch.tying.tie_updated = False - branch.tying.guide_target.add_branch() - - # Update the L-System string with tying modifications - lstring, modifications_count = branch.tie_lstring(lstring, position) - - return True + """Backward compatibility wrapper for tie.""" + sim = EnvySimulation(simulation_config) + return sim.tie(lstring) + - return False \ No newline at end of file diff --git a/examples/UFO/UFO.lpy b/examples/UFO/UFO.lpy index 9396790..642a68e 100644 --- a/examples/UFO/UFO.lpy +++ b/examples/UFO/UFO.lpy @@ -202,13 +202,13 @@ grow_object(plant_segment) : for module in plant_segment.post_bud_rule(plant_segment, simulation_config): nproduce new(module[0], *module[1]) - if should_bud(plant_segment, simulation_config): - # Age-based bud production for lateral branching - nproduce bud(ParameterSet(type = plant_segment, num_buds = 0)) - - if plant_segment.post_bud_rule(plant_segment, simulation_config): - for module in plant_segment.post_bud_rule(plant_segment, simulation_config): - nproduce new(module[0], *module[1]) + if should_bud(plant_segment, simulation_config): + # Age-based bud production for lateral branching + nproduce bud(ParameterSet(type = plant_segment, num_buds = 0)) + + if plant_segment.post_bud_rule(plant_segment, simulation_config): + for module in plant_segment.post_bud_rule(plant_segment, simulation_config): + nproduce new(module[0], *module[1]) produce grow_object(plant_segment) diff --git a/examples/UFO/UFO_prototypes.py b/examples/UFO/UFO_prototypes.py index abcf9ba..a6d3fed 100644 --- a/examples/UFO/UFO_prototypes.py +++ b/examples/UFO/UFO_prototypes.py @@ -120,7 +120,7 @@ def post_bud_rule(self, plant_segment, simulation_config ): growth_length=0.05, thickness_increment=0., color=[0, 255, 0], - bud_spacing_age=2, # Spurs bud every 1 age unit + bud_spacing_age=1, # Spurs bud every 1 age unit curve_x_range=(-0.2, 0.2), # Tighter bounds for spur curves curve_y_range=(-0.2, 0.2), # Tighter bounds for spur curves curve_z_range=(-1, 1) # Same Z range @@ -151,7 +151,8 @@ def post_bud_rule(self, plant_segment, simulation_config ): bud_spacing_age=2, # Trunk buds every 4 age units curve_x_range=(-0.3, 0.3), # Conservative bounds for trunk curve_y_range=(-0.3, 0.3), # Conservative bounds for trunk - curve_z_range=(-0.5, 0.5) # Tighter Z range for trunk + curve_z_range=(-0.5, 0.5), # Tighter Z range for trunk + prunable=False ) branch_config = BasicWoodConfig( diff --git a/examples/UFO/UFO_simulation.py b/examples/UFO/UFO_simulation.py index 5323c95..1fdcf32 100644 --- a/examples/UFO/UFO_simulation.py +++ b/examples/UFO/UFO_simulation.py @@ -1,299 +1,91 @@ +import sys +sys.path.append('../../') from dataclasses import dataclass import numpy as np -from helper import cut_from +from simulation_base import SimulationConfig, TreeSimulationBase @dataclass -class UFOSimulationConfig: +class UFOSimulationConfig(SimulationConfig): """Configuration for UFO trellis tree simulation parameters.""" - # Tying and Pruning + # Override base defaults for UFO-specific values num_iteration_tie: int = 8 num_iteration_prune: int = 16 + pruning_age_threshold: int = 8 + derivation_length: int = 160 - # Display - label: bool = True - - # Support Structure + # UFO-specific Support Structure support_trunk_wire_point: tuple = (0.6, 0, 0.4) support_num_wires: int = 7 - support_spacing_wires: int = 1 - # Point Generation + # UFO-specific Point Generation ufo_x_range: tuple = (0.65, 3) ufo_x_spacing: float = 0.3 ufo_z_value: float = 1.4 ufo_y_value: float = 0 - # Energy and Tying Parameters - energy_distance_weight: float = 0.5 # Weight for distance in energy calculation (was hardcoded /2) - energy_threshold: float = 1.0 # Maximum energy threshold for tying - - # Pruning Parameters - pruning_age_threshold: int = 8 # Age threshold for pruning untied branches - - # L-System Parameters - derivation_length: int = 160 # Number of derivation steps - - # Growth Parameters + # UFO-specific Growth Parameters thickness_multiplier: float = 1.2 # Multiplier for internode thickness - tolerance: float = 1e-6 # Tolerance for age-based bud spacing - - # Visualization Parameters - attractor_point_width: int = 10 # Width of attractor points in visualization - -def generate_points_ufo(simulation_config): - """ - Generate 3D points for the UFO trellis wire structure. - - Creates a linear array of wire attachment points along the x-axis at a fixed - height (z) and depth (y). The points are spaced evenly within the configured - x-range and used to construct the trellis support structure. - - Returns: - list: List of (x, y, z) tuples representing wire attachment points, - where all points share the same y and z coordinates. - - Configuration parameters used: - - ufo_x_range: Tuple (min_x, max_x) defining the range of x coordinates - - ufo_x_spacing: Spacing between consecutive x coordinates - - ufo_z_value: Fixed z-coordinate (height) for all points - - ufo_y_value: Fixed y-coordinate (depth) for all points - """ - x = np.arange( - simulation_config.ufo_x_range[0], - simulation_config.ufo_x_range[1], - simulation_config.ufo_x_spacing - ).astype(float) - z = np.full((x.shape[0],), simulation_config.ufo_z_value).astype(float) - y = np.full((x.shape[0],), simulation_config.ufo_y_value).astype(float) - - wire_attachment_points = [] - for point_index in range(x.shape[0]): - wire_attachment_points.append((x[point_index], y[point_index], z[point_index])) - - return wire_attachment_points - -def get_energy_mat(branches, arch, simulation_config): +class UFOSimulation(TreeSimulationBase): """ - Calculate the energy matrix for optimal branch-to-wire assignment. - - This function computes an energy cost matrix where each entry represents the - "cost" of assigning a specific branch to a specific wire in the trellis system. - The energy is based on the Euclidean distance from wire attachment points to - both the start and end points of each branch, weighted by the simulation's - distance weight parameter. + UFO trellis architecture simulation. - The algorithm uses a greedy optimization approach where branches are assigned - to the lowest-energy available wire that hasn't reached capacity. - - Args: - branches: List of branch objects to be assigned to wires - arch: Support architecture object containing wire information - - Returns: - numpy.ndarray: Energy matrix of shape (num_branches, num_wires) where - matrix[i][j] is the energy cost of assigning branch i to wire j. - Untied branches and occupied wires have infinite energy (np.inf). + Implements the UFO (Upright Fruiting Offshoots) training system with + horizontal wires arranged linearly along the x-axis. """ - num_branches = len(branches) - num_wires = len(arch.branch_supports) - # Initialize energy matrix with infinite values (impossible assignments) - energy_matrix = np.full((num_branches, num_wires), np.inf) - - # Calculate energy costs for all valid branch-wire combinations - for branch_idx, branch in enumerate(branches): - # Skip branches that are already tied - if branch.tying.has_tied: - continue - - for wire_id, wire in arch.branch_supports.items(): - # Skip wires that already have a branch attached - if wire.num_branch >= 1: - continue - - # Calculate weighted distance energy for this branch-wire pair - # Energy considers distance from wire to both branch endpoints - wire_point = np.array(wire.point) - branch_start = np.array(branch.location.start) - branch_end = np.array(branch.location.end) - - start_distance_energy = np.sum((wire_point - branch_start) ** 2) - end_distance_energy = np.sum((wire_point - branch_end) ** 2) - - total_energy = (start_distance_energy + end_distance_energy) * simulation_config.energy_distance_weight - - energy_matrix[branch_idx, wire_id] = total_energy - - return energy_matrix - -def decide_guide(energy_matrix, branches, arch, simulation_config): - """ - Perform greedy assignment of branches to wires based on energy matrix. - - This function implements a greedy optimization algorithm that iteratively assigns - the branch-wire pair with the lowest energy cost. Once a branch is assigned to - a wire, both that branch and wire are marked as unavailable (infinite energy) - to prevent further assignments. - - The algorithm continues until no valid assignments remain (all remaining energies - are infinite or above the threshold). - - Args: - energy_matrix: numpy.ndarray of shape (num_branches, num_wires) with energy costs - branches: List of branch objects to be assigned - arch: Support architecture containing wire information + def generate_points(self): + """ + Generate 3D points for the UFO trellis wire structure. - Returns: - None: Modifies branches and arch in-place with new assignments - """ - num_branches, num_wires = energy_matrix.shape - - # Early return if no branches or wires to assign - if num_branches == 0 or num_wires == 0: - return - - # Continue making assignments until no valid ones remain - while True: - # Find the minimum energy value and its position - min_energy_indices = np.argwhere(energy_matrix == np.min(energy_matrix)) + Creates a linear array of wire attachment points along the x-axis at a fixed + height (z) and depth (y). The points are spaced evenly within the configured + x-range and used to construct the trellis support structure. - # If no valid indices found or matrix is empty, stop - if len(min_energy_indices) == 0: - break - - # Get the first (and typically only) minimum energy position - branch_idx, wire_id = min_energy_indices[0] - min_energy = energy_matrix[branch_idx, wire_id] + Returns: + list: List of (x, y, z) tuples representing wire attachment points, + where all points share the same y and z coordinates. + """ + x = np.arange( + self.config.ufo_x_range[0], + self.config.ufo_x_range[1], + self.config.ufo_x_spacing + ).astype(float) + z = np.full((x.shape[0],), self.config.ufo_z_value).astype(float) + y = np.full((x.shape[0],), self.config.ufo_y_value).astype(float) - # Stop if minimum energy is infinite (no valid assignments) or above threshold - if np.isinf(min_energy) or min_energy > simulation_config.energy_threshold: - break - - # Get the branch and wire objects - branch = branches[branch_idx] - wire = arch.branch_supports[wire_id] + wire_attachment_points = [] + for point_index in range(x.shape[0]): + wire_attachment_points.append((x[point_index], y[point_index], z[point_index])) - # Skip if branch is already tied (defensive check) - if branch.tying.has_tied: - # Mark this assignment as invalid and continue - energy_matrix[branch_idx, wire_id] = np.inf - continue - - # Perform the assignment - branch.tying.guide_target = wire - wire.add_branch() - - # Mark branch and wire as unavailable for future assignments - # Set entire row (branch) to infinity - this branch can't be assigned again - energy_matrix[branch_idx, :] = np.inf - # Set entire column (wire) to infinity - this wire can't accept more branches - energy_matrix[:, wire_id] = np.inf - - -def prune(lstring, simulation_config): - """ - Prune old branches that exceed the age threshold and haven't been tied to wires. - - This function implements the pruning strategy for the tree training simulation. - It identifies branches that have grown too old (exceeding the pruning age threshold) - but haven't been successfully tied to trellis wires. Such branches are considered - unproductive and are removed from the L-System to encourage new growth. - - The pruning criteria are: - 1. Branch age exceeds the configured pruning threshold - 2. Branch has not been tied to any trellis wire - 3. Branch has not already been marked for cutting - - When a branch meets all criteria, it is: - - Marked as cut (to prevent re-processing) - - Removed from the L-System string using cut_from() - - Args: - lstring: The current L-System string containing modules and their parameters - - Returns: - bool: True if a branch was pruned, False if no eligible branches found - - Note: - This function processes one branch at a time and returns immediately after - pruning a single branch. It should be called repeatedly (e.g., in a while loop) - until no more pruning operations are possible. The cut_from() function handles - the actual removal of the branch and any dependent substructures from the string. - """ - for position, symbol in enumerate(lstring): - # Check if this is a WoodStart module (represents a branch) - if symbol.name == 'WoodStart': - branch = symbol[0].type + return wire_attachment_points - # Check pruning criteria - age_exceeds_threshold = branch.info.age > simulation_config.pruning_age_threshold - not_tied_to_wire = not branch.tying.has_tied - not_already_cut = not branch.info.cut - is_prunable = branch.info.prunable - # Prune if all criteria are met - if age_exceeds_threshold and not_tied_to_wire and not_already_cut and is_prunable: - # Mark branch as cut to prevent re-processing - branch.info.cut = True +# Backwards compatibility: provide standalone functions that use the class +# Backwards compatibility: provide standalone functions that use the class +def generate_points_ufo(simulation_config): + """Backward compatibility wrapper for generate_points.""" + sim = UFOSimulation(simulation_config) + return sim.generate_points() - # Remove the branch from the L-System string - lstring = cut_from(position, lstring) +def get_energy_mat(branches, arch, simulation_config): + """Backward compatibility wrapper for get_energy_mat.""" + sim = UFOSimulation(simulation_config) + return sim.get_energy_mat(branches, arch) - return True +def decide_guide(energy_matrix, branches, arch, simulation_config): + """Backward compatibility wrapper for decide_guide.""" + sim = UFOSimulation(simulation_config) + return sim.decide_guide(energy_matrix, branches, arch) - return False +def prune(lstring, simulation_config): + """Backward compatibility wrapper for prune.""" + sim = UFOSimulation(simulation_config) + return sim.prune(lstring) def tie(lstring, simulation_config): - """ - Perform tying operation on eligible branches in the L-System string. - - This function searches through the L-System string for 'WoodStart' modules that - represent branches ready for tying to trellis wires. It identifies branches that: - 1. Have tying properties (tying attribute exists) - 2. Have a defined tie axis (tie_axis is not None) - 3. Have not been tied yet (tie_updated is False) - 4. Have guide points available for wire attachment - - When an eligible branch is found, it performs the tying operation by: - - Marking the branch as tied (tie_updated = False) - - Adding the branch to the target wire - - Calling the branch's tie_lstring method to modify the L-System string - - Args: - lstring: The current L-System string containing modules and their parameters - - Returns: - bool: True if a tying operation was performed, False if no eligible branches found - - Note: - This function processes one branch at a time and returns immediately after - tying a single branch. It should be called repeatedly (e.g., in a while loop) - until no more tying operations are possible. - """ - for position, symbol in enumerate(lstring): - # Check if this is a WoodStart module with tying capabilities - if (symbol == 'WoodStart' and - hasattr(symbol[0].type, 'tying') and - getattr(symbol[0].type.tying, 'tie_axis', None) is not None): - - branch = symbol[0].type - - # Skip branches that have already been processed for tying - if not branch.tying.tie_updated: - continue - - # Check if branch has guide points for wire attachment - if branch.tying.guide_points: - # Perform the tying operation - branch.tying.tie_updated = False - branch.tying.guide_target.add_branch() - - # Update the L-System string with tying modifications - lstring, modifications_count = branch.tie_lstring(lstring, position) - - return True - - return False \ No newline at end of file + """Backward compatibility wrapper for tie.""" + sim = UFOSimulation(simulation_config) + return sim.tie(lstring) diff --git a/simulation_base.py b/simulation_base.py new file mode 100644 index 0000000..621787a --- /dev/null +++ b/simulation_base.py @@ -0,0 +1,315 @@ +""" +Base simulation module for L-System tree training and trellis systems. + +This module provides common functionality for tree architecture simulations including: +- Energy-based branch-to-wire optimization +- Tying operations for attaching branches to trellis wires +- Pruning strategies for untied branches + +Architecture-specific implementations (Envy, UFO, etc.) should inherit from this base +and implement architecture-specific methods like point generation. +""" + +from abc import ABC, abstractmethod +from dataclasses import dataclass +import numpy as np +from helper import cut_from + + +@dataclass +class SimulationConfig(ABC): + """Base configuration class for tree training simulations. + + Architecture-specific configs should inherit from this and add their own parameters. + Common parameters across all architectures are defined here. + """ + + # Tying and Pruning Intervals + num_iteration_tie: int = 5 + num_iteration_prune: int = 16 + + # Display Options + label: bool = True + + # Support Structure + support_num_wires: int = 14 + support_spacing_wires: int = 1 + support_trunk_wire_point: tuple = None + + # Energy and Tying Parameters + energy_distance_weight: float = 0.5 # Weight for distance in energy calculation + energy_threshold: float = 1.0 # Maximum energy threshold for tying + + # Pruning Parameters + pruning_age_threshold: int = 6 # Age threshold for pruning untied branches + + # L-System Parameters + derivation_length: int = 128 # Number of derivation steps + + # Growth Parameters + tolerance: float = 0.01 # Tolerance for age-based bud spacing (bud_age_tolerance) + + # Visualization Parameters + attractor_point_width: int = 10 # Width of attractor points in visualization + + +class TreeSimulationBase(ABC): + """ + Base class for tree architecture simulations with trellis training. + + This class provides common algorithms for: + - Energy-based optimization for branch-to-wire assignment + - Greedy assignment of branches to wires + - Pruning operations for untied branches + - Tying operations to modify L-System strings + + Architecture-specific implementations should: + 1. Inherit from this class + 2. Implement generate_points() for their specific trellis layout + 3. Optionally override methods if custom behavior is needed + """ + + def __init__(self, config: SimulationConfig): + """ + Initialize the simulation with a configuration object. + + Args: + config: SimulationConfig instance with parameters for the simulation + """ + self.config = config + + @abstractmethod + def generate_points(self): + """ + Generate 3D points for the trellis wire structure. + + This method must be implemented by architecture-specific subclasses + to define the layout of trellis wires (V-trellis, UFO, etc.). + + Returns: + list: List of (x, y, z) tuples representing wire attachment points + """ + pass + + def get_energy_mat(self, branches, arch): + """ + Calculate the energy matrix for optimal branch-to-wire assignment. + + This function computes an energy cost matrix where each entry represents the + "cost" of assigning a specific branch to a specific wire in the trellis system. + The energy is based on the Euclidean distance from wire attachment points to + both the start and end points of each branch, weighted by the simulation's + distance weight parameter. + + The algorithm uses a greedy optimization approach where branches are assigned + to the lowest-energy available wire that hasn't reached capacity. + + Args: + branches: List of branch objects to be assigned to wires + arch: Support architecture object containing wire information + + Returns: + numpy.ndarray: Energy matrix of shape (num_branches, num_wires) where + matrix[i][j] is the energy cost of assigning branch i to wire j. + Untied branches and occupied wires have infinite energy (np.inf). + """ + num_branches = len(branches) + num_wires = len(arch.branch_supports) + + # Initialize energy matrix with infinite values (impossible assignments) + energy_matrix = np.full((num_branches, num_wires), np.inf) + + # Calculate energy costs for all valid branch-wire combinations + for branch_idx, branch in enumerate(branches): + # Skip branches that are already tied + if branch.tying.has_tied: + continue + + for wire_id, wire in arch.branch_supports.items(): + # Skip wires that already have a branch attached + if wire.num_branch >= 1: + continue + + # Calculate weighted distance energy for this branch-wire pair + # Energy considers distance from wire to both branch endpoints + wire_point = np.array(wire.point) + branch_start = np.array(branch.location.start) + branch_end = np.array(branch.location.end) + + start_distance_energy = np.sum((wire_point - branch_start) ** 2) + end_distance_energy = np.sum((wire_point - branch_end) ** 2) + + total_energy = (start_distance_energy + end_distance_energy) * self.config.energy_distance_weight + + energy_matrix[branch_idx, wire_id] = total_energy + + return energy_matrix + + def decide_guide(self, energy_matrix, branches, arch): + """ + Perform greedy assignment of branches to wires based on energy matrix. + + This function implements a greedy optimization algorithm that iteratively assigns + the branch-wire pair with the lowest energy cost. Once a branch is assigned to + a wire, both that branch and wire are marked as unavailable (infinite energy) + to prevent further assignments. + + The algorithm continues until no valid assignments remain (all remaining energies + are infinite or above the threshold). + + Args: + energy_matrix: numpy.ndarray of shape (num_branches, num_wires) with energy costs + branches: List of branch objects to be assigned + arch: Support architecture containing wire information + + Returns: + None: Modifies branches and arch in-place with new assignments + """ + num_branches, num_wires = energy_matrix.shape + + # Early return if no branches or wires to assign + if num_branches == 0 or num_wires == 0: + return + + # Continue making assignments until no valid ones remain + while True: + # Find the minimum energy value and its position + min_energy_indices = np.argwhere(energy_matrix == np.min(energy_matrix)) + + # If no valid indices found or matrix is empty, stop + if len(min_energy_indices) == 0: + break + + # Get the first (and typically only) minimum energy position + branch_idx, wire_id = min_energy_indices[0] + min_energy = energy_matrix[branch_idx, wire_id] + + # Stop if minimum energy is infinite (no valid assignments) or above threshold + if np.isinf(min_energy) or min_energy > self.config.energy_threshold: + break + + # Get the branch and wire objects + branch = branches[branch_idx] + wire = arch.branch_supports[wire_id] + + # Skip if branch is already tied (defensive check) + if branch.tying.has_tied: + # Mark this assignment as invalid and continue + energy_matrix[branch_idx, wire_id] = np.inf + continue + + # Perform the assignment + branch.tying.guide_target = wire + wire.add_branch() + + # Mark branch and wire as unavailable for future assignments + # Set entire row (branch) to infinity - this branch can't be assigned again + energy_matrix[branch_idx, :] = np.inf + # Set entire column (wire) to infinity - this wire can't accept more branches + energy_matrix[:, wire_id] = np.inf + + def prune(self, lstring): + """ + Prune old branches that exceed the age threshold and haven't been tied to wires. + + This function implements the pruning strategy for the tree training simulation. + It identifies branches that have grown too old (exceeding the pruning age threshold) + but haven't been successfully tied to trellis wires. Such branches are considered + unproductive and are removed from the L-System to encourage new growth. + + The pruning criteria are: + 1. Branch age exceeds the configured pruning threshold + 2. Branch has not been tied to any trellis wire + 3. Branch has not already been marked for cutting + 4. Branch is prunable (respects the prunable flag) + + When a branch meets all criteria, it is: + - Marked as cut (to prevent re-processing) + - Removed from the L-System string using cut_from() + + Args: + lstring: The current L-System string containing modules and their parameters + + Returns: + bool: True if a branch was pruned, False if no eligible branches found + + Note: + This function processes one branch at a time and returns immediately after + pruning a single branch. It should be called repeatedly (e.g., in a while loop) + until no more pruning operations are possible. The cut_from() function handles + the actual removal of the branch and any dependent substructures from the string. + """ + for position, symbol in enumerate(lstring): + # Check if this is a WoodStart module (represents a branch) + if symbol.name == 'WoodStart': + branch = symbol[0].type + + # Check pruning criteria + age_exceeds_threshold = branch.info.age > self.config.pruning_age_threshold + not_tied_to_wire = not branch.tying.has_tied + not_already_cut = not branch.info.cut + is_prunable = branch.info.prunable + + # Prune if all criteria are met + if age_exceeds_threshold and not_tied_to_wire and not_already_cut and is_prunable: + # Mark branch as cut to prevent re-processing + branch.info.cut = True + + # Remove the branch from the L-System string + lstring = cut_from(position, lstring) + + return True + + return False + + def tie(self, lstring): + """ + Perform tying operation on eligible branches in the L-System string. + + This function searches through the L-System string for 'WoodStart' modules that + represent branches ready for tying to trellis wires. It identifies branches that: + 1. Have tying properties (tying attribute exists) + 2. Have a defined tie axis (tie_axis is not None) + 3. Have not been tied yet (tie_updated is False) + 4. Have guide points available for wire attachment + + When an eligible branch is found, it performs the tying operation by: + - Marking the branch as tied (tie_updated = False) + - Adding the branch to the target wire + - Calling the branch's tie_lstring method to modify the L-System string + + Args: + lstring: The current L-System string containing modules and their parameters + + Returns: + bool: True if a tying operation was performed, False if no eligible branches found + + Note: + This function processes one branch at a time and returns immediately after + tying a single branch. It should be called repeatedly (e.g., in a while loop) + until no more tying operations are possible. + """ + for position, symbol in enumerate(lstring): + # Check if this is a WoodStart module with tying capabilities + if (symbol == 'WoodStart' and + hasattr(symbol[0].type, 'tying') and + getattr(symbol[0].type.tying, 'tie_axis', None) is not None): + + branch = symbol[0].type + + # Skip branches that have already been processed for tying + if not branch.tying.tie_updated: + continue + + # Check if branch has guide points for wire attachment + if branch.tying.guide_points: + # Perform the tying operation + branch.tying.tie_updated = False + branch.tying.guide_target.add_branch() + + # Update the L-System string with tying modifications + lstring, modifications_count = branch.tie_lstring(lstring, position) + + return True + + return False From 9449c71bc2062085e1c452db74ba78ace819a53d Mon Sep 17 00:00:00 2001 From: Abhinav Jain Date: Mon, 17 Nov 2025 23:09:03 -0800 Subject: [PATCH 11/14] Move to nodes per segment and fixed cylinder length --- examples/Envy/Envy.lpy | 9 +++++--- examples/Envy/Envy_prototypes.py | 28 ++++++++++++++--------- examples/UFO/UFO.lpy | 19 +++++++++------- examples/UFO/UFO_prototypes.py | 38 +++++++++++++++++--------------- simulation_base.py | 1 - stochastic_tree.py | 8 +++++-- 6 files changed, 61 insertions(+), 42 deletions(-) diff --git a/examples/Envy/Envy.lpy b/examples/Envy/Envy.lpy index d64a854..26eb801 100644 --- a/examples/Envy/Envy.lpy +++ b/examples/Envy/Envy.lpy @@ -195,9 +195,12 @@ grow_object(plant_segment) : # Add color visualization if labeling is enabled -- Can this be moved to the start of building the segment? r, g, b = plant_segment.info.color nproduce SetColor(r,g,b) - #Produce internode segment - nproduce I(plant_segment.growth.growth_length, plant_segment.growth.thickness, plant_segment) - #Produce bud + #Produce internode segments (n cylinders per growth step) + n_cylinders = int(plant_segment.growth.growth_length / plant_segment.growth.cylinder_length) + for i in range(n_cylinders): + nproduce I(plant_segment.growth.cylinder_length, plant_segment.growth.thickness, plant_segment) + + #Produce bud (after all cylinders in this growth step) if plant_segment.pre_bud_rule(plant_segment, simulation_config): for module in plant_segment.post_bud_rule(plant_segment, simulation_config): nproduce new(module[0], *module[1]) diff --git a/examples/Envy/Envy_prototypes.py b/examples/Envy/Envy_prototypes.py index fdc8488..d5fb303 100644 --- a/examples/Envy/Envy_prototypes.py +++ b/examples/Envy/Envy_prototypes.py @@ -14,7 +14,9 @@ def __init__(self, config=None, copy_from=None, prototype_dict: dict = {}): super().__init__(config, copy_from, prototype_dict) def is_bud_break(self, num_buds_segment): - return (rd.random() < 0.1) + if num_buds_segment >= self.growth.max_buds_segment: + return False + return (rd.random() < 0.1 * (1 - num_buds_segment / self.growth.max_buds_segment)) def create_branch(self): return None @@ -29,15 +31,13 @@ def post_bud_rule(self, plant_segment, simulation_config): class Branch(TreeBranch): def __init__(self, config=None, copy_from=None, prototype_dict: dict = {}): super().__init__(config, copy_from, prototype_dict) - self.num_buds_segment = 0 def is_bud_break(self, num_break_buds): - if num_break_buds >= 1: + if num_break_buds >= self.growth.max_buds_segment: return False - return (rd.random() < 0.5*(1 - self.num_buds_segment/self.growth.max_buds_segment)) + return (rd.random() < 0.5 * (1 - num_break_buds / self.growth.max_buds_segment)) def create_branch(self): - self.num_buds_segment += 1 if rd.random() > 0.8: new_ob = NonTrunk(copy_from=self.prototype_dict['nontrunk']) else: @@ -57,7 +57,9 @@ def __init__(self, config=None, copy_from=None, prototype_dict: dict = {}): super().__init__(config, copy_from, prototype_dict) def is_bud_break(self, num_buds_segment): - if (rd.random() > 0.1*(1 - num_buds_segment/self.growth.max_buds_segment)): + if num_buds_segment >= self.growth.max_buds_segment: + return False + if (rd.random() > 0.1 * (1 - num_buds_segment / self.growth.max_buds_segment)): return False return True @@ -79,7 +81,9 @@ def __init__(self, config=None, copy_from=None, prototype_dict: dict = {}): super().__init__(config, copy_from, prototype_dict) def is_bud_break(self, num_buds_segment): - return (rd.random() < 0.5*(1 - num_buds_segment/self.growth.max_buds_segment)) + if num_buds_segment >= self.growth.max_buds_segment: + return False + return (rd.random() < 0.5 * (1 - num_buds_segment / self.growth.max_buds_segment)) def create_branch(self): if rd.random() > 0.3: @@ -104,7 +108,8 @@ def post_bud_rule(self, plant_segment, simulation_config): tie_axis=None, max_length=0.2, thickness=0.003, - growth_length=0.05, # growth_length/2 from original + growth_length=0.05, + cylinder_length=0.05, thickness_increment=0., color=[0, 255, 0], bud_spacing_age=2, @@ -115,11 +120,12 @@ def post_bud_rule(self, plant_segment, simulation_config): ) branch_config = BasicWoodConfig( - max_buds_segment=30, + max_buds_segment=2, tie_axis=(1, 0, 0), max_length=2.2, thickness=0.01, growth_length=0.1, + cylinder_length=0.05, thickness_increment=0.00001, color=[255, 150, 0], bud_spacing_age=2, @@ -135,6 +141,7 @@ def post_bud_rule(self, plant_segment, simulation_config): max_length=4, thickness=0.01, growth_length=0.1, + cylinder_length=0.05, thickness_increment=0.00001, color=[255, 0, 0], bud_spacing_age=2, @@ -149,7 +156,8 @@ def post_bud_rule(self, plant_segment, simulation_config): tie_axis=None, max_length=0.3, thickness=0.003, - growth_length=0.05, # growth_length/2 from original + growth_length=0.05, + cylinder_length=0.05, thickness_increment=0.00001, color=[0, 255, 0], bud_spacing_age=2, diff --git a/examples/UFO/UFO.lpy b/examples/UFO/UFO.lpy index 642a68e..52c6bd8 100644 --- a/examples/UFO/UFO.lpy +++ b/examples/UFO/UFO.lpy @@ -115,11 +115,11 @@ def EndEach(lstring): while tie(lstring, simulation_config): pass # Continue until no more tying operations are possible - # Check if this is also a pruning iteration - if current_iteration % pruning_interval_iterations == 0: - # Prune branches until no more can be pruned - while prune(lstring, simulation_config): - pass + # Check if this is also a pruning iteration + if current_iteration % pruning_interval_iterations == 0: + # Prune branches until no more can be pruned + while prune(lstring, simulation_config): + pass return lstring @@ -195,9 +195,12 @@ grow_object(plant_segment) : # Add color visualization if labeling is enabled -- Can this be moved to the start of building the segment? r, g, b = plant_segment.info.color nproduce SetColor(r,g,b) - #Produce internode segment - nproduce I(plant_segment.growth.growth_length, plant_segment.growth.thickness, plant_segment) - #Produce bud + #Produce internode segments (n cylinders per growth step) + n_cylinders = int(plant_segment.growth.growth_length / plant_segment.growth.cylinder_length) + for i in range(n_cylinders): + nproduce I(plant_segment.growth.cylinder_length, plant_segment.growth.thickness, plant_segment) + + #Produce bud (after all cylinders in this growth step) if plant_segment.pre_bud_rule(plant_segment, simulation_config): for module in plant_segment.post_bud_rule(plant_segment, simulation_config): nproduce new(module[0], *module[1]) diff --git a/examples/UFO/UFO_prototypes.py b/examples/UFO/UFO_prototypes.py index a6d3fed..82c1077 100644 --- a/examples/UFO/UFO_prototypes.py +++ b/examples/UFO/UFO_prototypes.py @@ -15,7 +15,7 @@ def __init__(self, config=None, copy_from=None, prototype_dict: dict = {}): def is_bud_break(self, num_buds_segment): if num_buds_segment >= self.growth.max_buds_segment: return False - return (rd.random() < 0.1) + return (rd.random() < 0.1 * (1 - num_buds_segment / self.growth.max_buds_segment)) def create_branch(self): return None @@ -36,10 +36,9 @@ def __init__(self, config=None, copy_from=None, prototype_dict: dict = {}): super().__init__(config, copy_from, prototype_dict) def is_bud_break(self, num_buds_segment): - if num_buds_segment >= 2: + if num_buds_segment >= self.growth.max_buds_segment: return False - if (rd.random() < 0.005*self.growth.growth_length*(1 - self.num_buds/self.growth.max_buds_segment)): - self.num_buds +=1 + if (rd.random() < 0.005*self.growth.growth_length * (1 - num_buds_segment / self.growth.max_buds_segment)): return True def create_branch(self): @@ -60,9 +59,9 @@ def __init__(self, config=None, copy_from=None, prototype_dict: dict = {}): super().__init__(config, copy_from, prototype_dict) def is_bud_break(self, num_buds_segment): - if num_buds_segment >= 2: + if num_buds_segment >= self.growth.max_buds_segment: return False - if (rd.random() < 0.2*(1 - self.num_buds/self.growth.max_buds_segment)): + if (rd.random() < 0.2 * (1 - num_buds_segment / self.growth.max_buds_segment)): return True @@ -91,9 +90,8 @@ def __init__(self, config=None, copy_from=None, prototype_dict: dict = {}): def is_bud_break(self, num_buds_segment): if num_buds_segment >= self.growth.max_buds_segment: return False - if (rd.random() > 0.05*self.length/self.growth.max_length*(1 - self.num_buds/self.growth.max_buds_segment)): + if (rd.random() > 0.05*self.length/self.growth.max_length * (1 - num_buds_segment / self.growth.max_buds_segment)): return False - self.num_buds+=1 return True def create_branch(self): @@ -113,12 +111,13 @@ def post_bud_rule(self, plant_segment, simulation_config ): # Create configs for cleaner prototype setup spur_config = BasicWoodConfig( - max_buds_segment=5, + max_buds_segment=2, tie_axis=None, max_length=0.1, thickness=0.003, - growth_length=0.05, - thickness_increment=0., + growth_length=0.05, + cylinder_length=0.01, + thickness_increment=0., color=[0, 255, 0], bud_spacing_age=1, # Spurs bud every 1 age unit curve_x_range=(-0.2, 0.2), # Tighter bounds for spur curves @@ -127,12 +126,13 @@ def post_bud_rule(self, plant_segment, simulation_config ): ) side_branch_config = BasicWoodConfig( - max_buds_segment=40, + max_buds_segment=2, tie_axis=None, max_length=0.25, thickness=0.003, - growth_length=0.05, - thickness_increment=0.00001, + growth_length=0.05, + cylinder_length=0.01, + thickness_increment=0.00001, color=[0, 255, 0], bud_spacing_age=2, # Tertiary branches bud every 3 age units curve_x_range=(-0.5, 0.5), # Moderate bounds for tertiary branches @@ -141,12 +141,13 @@ def post_bud_rule(self, plant_segment, simulation_config ): ) trunk_config = BasicWoodConfig( - max_buds_segment=60, + max_buds_segment=5, tie_axis=(1, 0, 0), max_length=3, thickness=0.02, thickness_increment=0.00001, - growth_length=0.1, + growth_length=0.1, + cylinder_length=0.02, color=[255, 0, 0], bud_spacing_age=2, # Trunk buds every 4 age units curve_x_range=(-0.3, 0.3), # Conservative bounds for trunk @@ -156,12 +157,13 @@ def post_bud_rule(self, plant_segment, simulation_config ): ) branch_config = BasicWoodConfig( - max_buds_segment=140, + max_buds_segment=2, tie_axis=(0, 0, 1), max_length=2.5, thickness=0.01, thickness_increment=0.00001, - growth_length=0.1, + growth_length=0.1, + cylinder_length=0.02, color=[255, 150, 0], bud_spacing_age=2, # Branches bud every 2 age units curve_x_range=(-0.4, 0.4), # Moderate bounds for primary branches diff --git a/simulation_base.py b/simulation_base.py index 621787a..9e94c58 100644 --- a/simulation_base.py +++ b/simulation_base.py @@ -305,7 +305,6 @@ def tie(self, lstring): if branch.tying.guide_points: # Perform the tying operation branch.tying.tie_updated = False - branch.tying.guide_target.add_branch() # Update the L-System string with tying modifications lstring, modifications_count = branch.tie_lstring(lstring, position) diff --git a/stochastic_tree.py b/stochastic_tree.py index f3d2f14..37e025d 100644 --- a/stochastic_tree.py +++ b/stochastic_tree.py @@ -48,10 +48,11 @@ def __post_init__(self): @dataclass class GrowthState: """Growth parameters for a wood object.""" - max_buds_segment: int = 5 + max_buds_segment: int = 5 # Total cumulative buds allowed across the entire branch segment (not per node) thickness: float = 0.1 thickness_increment: float = 0.01 growth_length: float = 1.0 + cylinder_length: float = 0.1 max_length: float = 7.0 @dataclass @@ -75,10 +76,11 @@ def __post_init__(self): class BasicWoodConfig: """Configuration parameters for BasicWood initialization.""" copy_from: any = None - max_buds_segment: int = 5 + max_buds_segment: int = 5 # Total cumulative buds allowed across the entire branch segment (not per node) thickness: float = 0.1 thickness_increment: float = 0.01 growth_length: float = 1.0 + cylinder_length: float = 0.1 # Length of each individual cylinder max_length: float = 7.0 tie_axis: tuple = None order: int = 0 @@ -130,6 +132,7 @@ def __init__(self, config=None, copy_from=None, **kwargs): curve_x_range = config.curve_x_range curve_y_range = config.curve_y_range curve_z_range = config.curve_z_range + cylinder_length = config.cylinder_length elif copy_from is None: raise ValueError("config must be provided when copy_from is None") @@ -150,6 +153,7 @@ def __init__(self, config=None, copy_from=None, **kwargs): thickness=thickness, thickness_increment=thickness_increment, growth_length=growth_length, + cylinder_length=cylinder_length, max_length=max_length ) # Bud spacing for L-System rules From 99309ea2224dced7e41015809719a0a4a56175f8 Mon Sep 17 00:00:00 2001 From: Abhinav Jain Date: Tue, 18 Nov 2025 00:48:29 -0800 Subject: [PATCH 12/14] Add base lpy file --- base_lpy.lpy | 225 +++++++++++++++++++++++++++++++ examples/Envy/Envy.lpy | 293 ++++------------------------------------- examples/UFO/UFO.lpy | 291 ++++------------------------------------ helper.py | 65 +++++++++ 4 files changed, 338 insertions(+), 536 deletions(-) create mode 100644 base_lpy.lpy diff --git a/base_lpy.lpy b/base_lpy.lpy new file mode 100644 index 0000000..849baa1 --- /dev/null +++ b/base_lpy.lpy @@ -0,0 +1,225 @@ +""" +Shared L-System driver for Envy, UFO, and future trellis architectures. +Configure via extern variables passed to Lsystem(..., variables). +""" +import os +import sys +import time +import numpy as np +import random as rd + +# Ensure repository root is importable regardless of working directory +CURRENT_DIR = os.path.dirname(__file__) +if CURRENT_DIR not in sys.path: + sys.path.append(CURRENT_DIR) + +from stochastic_tree import Support, BasicWood, TreeBranch, BasicWoodConfig +from helper import * +from dataclasses import dataclass + +# ----------------------------------------------------------------------------- +# EXTERNALLY CONFIGURABLE PATHS +# ----------------------------------------------------------------------------- +# Override these externs via Lsystem(..., variables) to point at a specific +# architecture without editing this shared file. +extern( + prototype_dict_path = "examples.Envy.Envy_prototypes.basicwood_prototypes", + trunk_class_path = "examples.Envy.Envy_prototypes.Trunk", + simulation_config_class_path = "examples.Envy.Envy_simulation.EnvySimulationConfig", + point_generator_path = "examples.Envy.Envy_simulation.generate_points_v_trellis", + energy_function_path = "examples.Envy.Envy_simulation.get_energy_mat", + decide_function_path = "examples.Envy.Envy_simulation.decide_guide", + tie_function_path = "examples.Envy.Envy_simulation.tie", + prune_function_path = "examples.Envy.Envy_simulation.prune", + axiom_pitch = 0.0, + axiom_yaw = 0.0, +) + +# Resolve extern-provided dotted paths so the rest of the script can operate on +# concrete classes/functions exactly like the Envy/UFO drivers do. +basicwood_prototypes = resolve_attr(prototype_dict_path) +Trunk = resolve_attr(trunk_class_path) +SimulationConfigClass = resolve_attr(simulation_config_class_path) +point_generator_fn = resolve_attr(point_generator_path) +get_energy_mat = resolve_attr(energy_function_path) +decide_guide = resolve_attr(decide_function_path) +tie = resolve_attr(tie_function_path) +prune = resolve_attr(prune_function_path) + +simulation_config = SimulationConfigClass() +main_trunk = Trunk(copy_from = basicwood_prototypes['trunk']) + +# Build trellis geometry and cadence numbers up front so callbacks stay simple. +trellis_support = Support( + point_generator_fn(simulation_config), + simulation_config.support_num_wires, + simulation_config.support_spacing_wires, + simulation_config.support_trunk_wire_point, +) +tying_interval_iterations = simulation_config.num_iteration_tie +pruning_interval_iterations = simulation_config.num_iteration_prune +main_trunk.tying.guide_target = trellis_support.trunk_wire + +# Track parent/child relationships for tying/pruning decisions. +branch_hierarchy = {main_trunk.name: []} +enable_color_labeling = simulation_config.label + +# L-Py callbacks delegate to the Python helpers so architectures share logic. +def StartEach(lstring): + """Proxy to shared tying preparation logic.""" + start_each_common(lstring, branch_hierarchy, trellis_support, main_trunk) + + +def EndEach(lstring): + """Proxy to shared tying/pruning orchestration logic.""" + return end_each_common( + lstring, + branch_hierarchy, + trellis_support, + tying_interval_iterations, + pruning_interval_iterations, + simulation_config, + main_trunk, + getIterationNb, + get_energy_mat, + decide_guide, + tie, + prune, + ) + +# ============================================================================= +# L-SYSTEM GRAMMAR DEFINITION +# ============================================================================= +# This section defines the formal grammar for the tree growth simulation. +# The L-System uses modules (symbols) to represent different plant components +# and their growth behaviors. + +# ----------------------------------------------------------------------------- +# MODULE DECLARATIONS +# ----------------------------------------------------------------------------- +# Define the vocabulary of symbols used in the L-System grammar. +# Each module represents a different type of plant component or operation. +module Attractors # Trellis support structure that guides branch growth +module grow_object # Growing plant parts (trunk, branches, spurs) with length/thickness +module bud # Dormant buds that can break to produce new branches +module branch # Branch segments in the L-System string +module WoodStart # Starting point of wood segments (used for tying operations) + +# ----------------------------------------------------------------------------- +# GLOBAL L-SYSTEM PARAMETERS +# ----------------------------------------------------------------------------- +# Create a growth guide curve for the initial trunk development +trunk_guide_curve = create_bezier_curve(x_range = (-1, 1), y_range = (-1, 1), z_range = (0, 10), seed_value=time.time()) + +# ----------------------------------------------------------------------------- +# AXIOM (STARTING STRING) +# ----------------------------------------------------------------------------- +# The initial L-System string that begins the simulation. +# Starts with the trellis attractors, sets up the trunk guide curve, +# and initializes the trunk growth with proper orientation. +Axiom: Attractors(trellis_support)SetGuide(trunk_guide_curve, main_trunk.growth.max_length)[@GcGetPos(main_trunk.location.start)WoodStart(ParameterSet(type = main_trunk))&(axiom_pitch)/(axiom_yaw)grow_object(main_trunk)GetPos(main_trunk.location.end)@Ge] + +# ----------------------------------------------------------------------------- +# DERIVATION PARAMETERS +# ----------------------------------------------------------------------------- +# Set the maximum number of derivation steps for the L-System +derivation length: simulation_config.derivation_length + +# ----------------------------------------------------------------------------- +# PRODUCTION RULES +# ----------------------------------------------------------------------------- +# Define how each module type evolves during each derivation step. +# These rules control the growth, branching, and development of the tree. + +production: + +# GROW_OBJECT PRODUCTION RULE +# Handles the growth of plant segments (trunk, branches, spurs) +# Determines whether to continue growing, stop, or produce buds +grow_object(plant_segment) : + if plant_segment == None: + # Null object - terminate this branch + produce * + if plant_segment.length >= plant_segment.growth.max_length: + # Maximum length reached - stop growing this segment + nproduce * + else: + # Continue growing - update segment properties + nproduce SetContour(plant_segment.contour) + #Update internal state of the plant segment + plant_segment.grow_one() + #Update physical representation + if enable_color_labeling: + # Add color visualization if labeling is enabled -- Can this be moved to the start of building the segment? + r, g, b = plant_segment.info.color + nproduce SetColor(r,g,b) + #Produce internode segments (n cylinders per growth step) + n_cylinders = int(plant_segment.growth.growth_length / plant_segment.growth.cylinder_length) + for i in range(n_cylinders): + nproduce I(plant_segment.growth.cylinder_length, plant_segment.growth.thickness, plant_segment) + #Produce bud (after all cylinders in this growth step) + if plant_segment.pre_bud_rule(plant_segment, simulation_config): + for generated in plant_segment.post_bud_rule(plant_segment, simulation_config): + nproduce new(generated[0], *generated[1]) + + if should_bud(plant_segment, simulation_config): + # Age-based bud production for lateral branching + nproduce bud(ParameterSet(type = plant_segment, num_buds = 0)) + + if plant_segment.post_bud_rule(plant_segment, simulation_config): + for generated in plant_segment.post_bud_rule(plant_segment, simulation_config): + nproduce new(generated[0], *generated[1]) + + produce grow_object(plant_segment) + +# BUD PRODUCTION RULE +# Controls bud break and branch initiation +# Determines when buds activate to produce new branches +bud(bud_parameters) : + if bud_parameters.type.is_bud_break(bud_parameters.num_buds): + # Bud break condition met - create new branch + + new_branch = bud_parameters.type.create_branch() + if new_branch == None: + # Branch creation failed - terminate + produce * + # Register new branch in parent-child relationship tracking + branch_hierarchy[new_branch.name] = [] + branch_hierarchy[bud_parameters.type.name].append(new_branch) + # Update branch counters + bud_parameters.num_buds+=1 + bud_parameters.type.info.num_branches+=1 + + if hasattr(new_branch, 'curve_x_range'): + # Curved branches: set up custom growth guide curve + curve = create_bezier_curve(x_range=new_branch.curve_x_range, y_range=new_branch.curve_y_range, z_range=new_branch.curve_z_range, seed_value=time.time()) + nproduce[@GcSetGuide(curve, new_branch.growth.max_length) + else: + # Straight branches: use default orientation + nproduce [@Gc + # Produce new branch with random orientation and growth object + nproduce @RGetPos(new_branch.location.start)WoodStart(ParameterSet(type = new_branch))/(rd.random()*360)&(rd.random()*360)grow_object(new_branch)GetPos(new_branch.location.end)@Ge]bud(bud_parameters) + +# ----------------------------------------------------------------------------- +# GEOMETRIC INTERPRETATION (HOMOMORPHISM) +# ----------------------------------------------------------------------------- +# Map abstract L-System modules to concrete 3D geometry for rendering. +# These rules define how the symbolic representation becomes visual. +homomorphism: + +# Internode segments become cylinders with length and radius +I(a,r,o) --> F(a,r) +# Branch segments also become cylinders +S(a,r,o) --> F(a,r) + +# ----------------------------------------------------------------------------- +# ATTRACTOR VISUALIZATION +# ----------------------------------------------------------------------------- +# Additional production rules for displaying trellis attractor points +production: +Attractors(trellis_support): + # Display enabled attractor points as visual markers + points_to_display = trellis_support.attractor_grid.get_enabled_points() + if len(points_to_display) > 0: + produce [,(3) @g(PointSet(points_to_display,width=simulation_config.attractor_point_width))] + diff --git a/examples/Envy/Envy.lpy b/examples/Envy/Envy.lpy index 26eb801..dbf3aee 100644 --- a/examples/Envy/Envy.lpy +++ b/examples/Envy/Envy.lpy @@ -1,269 +1,24 @@ -""" -Tying, Pruning and labelling Envy architecture trees -""" -import sys -sys.path.append('../../') -from stochastic_tree import Support, BasicWood, TreeBranch, BasicWoodConfig -import time -from helper import * -import numpy as np -import random as rd -from examples.Envy.Envy_prototypes import basicwood_prototypes, Trunk -from examples.Envy.Envy_simulation import EnvySimulationConfig, generate_points_v_trellis, get_energy_mat, decide_guide, tie, prune -from dataclasses import dataclass -import time - -#init -main_trunk = Trunk(copy_from = basicwood_prototypes['trunk']) - -# Create simulation configuration -simulation_config = EnvySimulationConfig() - - - -trellis_support = Support(generate_points_v_trellis(simulation_config), simulation_config.support_num_wires, simulation_config.support_spacing_wires, simulation_config.support_trunk_wire_point) -tying_interval_iterations = simulation_config.num_iteration_tie -pruning_interval_iterations = simulation_config.num_iteration_prune -main_trunk.tying.guide_target = trellis_support.trunk_wire - -def StartEach(lstring): - """ - Initialize tying updates for trunk and branches at the start of each iteration. - - This function is called at the beginning of each L-System derivation iteration - to prepare the tree structure for potential tying operations. It ensures that - both the trunk and all child branches are ready to participate in the tying - process by updating their tying status. - - The function performs two main tasks: - 1. Updates the trunk's tying status if it has a wire target and hasn't been updated - 2. Updates all child branches' tying status if they haven't been updated - - Args: - lstring: The current L-System string (not used in this function but required - by L-Py's callback interface) - - Returns: - None: Modifies global tree structures in-place - - Note: - This function relies on global variables: branch_hierarchy, trellis_support, main_trunk. - It should be called automatically by L-Py at the start of each iteration. - """ - global branch_hierarchy, trellis_support, main_trunk - - # Update trunk tying status if it has a wire target and needs updating - if trellis_support.trunk_wire and not main_trunk.tying.tie_updated: - main_trunk.tie_update() - - # Update tying status for all child branches that need it - for branch in branch_hierarchy[main_trunk.name]: - if not branch.tying.tie_updated: - branch.tie_update() - - -def EndEach(lstring): - """ - Perform tying and pruning operations at the end of each L-System iteration. - - This function is the main orchestration point for the tree training simulation. - It executes tying operations (assigning branches to trellis wires) and pruning - operations based on configurable iteration intervals. The tying process uses - energy-based optimization to find optimal branch-to-wire assignments. - - The function performs the following sequence when tying is scheduled: - 1. Updates the trunk's guide target (ties trunk first) - 2. Calculates energy matrix for all branch-wire combinations - 3. Uses greedy optimization to assign branches to lowest-energy wires - 4. Updates guide targets for all assigned branches - 5. Performs actual tying operations in the L-System string - 6. Prunes old branches if pruning iteration is reached - - Args: - lstring: The current L-System string containing modules and their parameters - - Returns: - The modified L-System string after tying and pruning operations - - Note: - This function relies on global variables and is called automatically by L-Py - at the end of each derivation iteration. Tying occurs every tying_interval_iterations - iterations, while pruning occurs every pruning_interval_iterations iterations. - """ - global branch_hierarchy, trellis_support, tying_interval_iterations - - current_iteration = getIterationNb() + 1 #GetIterationNb is an L-Py function - - # Check if this is a tying iteration - if current_iteration % tying_interval_iterations == 0: - # Tie trunk to its target wire first (one iteration before branches) - if trellis_support.trunk_wire: - main_trunk.update_guide(main_trunk.tying.guide_target) - - # Calculate energy costs for optimal branch-to-wire assignments - branches = branch_hierarchy[main_trunk.name] - energy_matrix = get_energy_mat(branches, trellis_support, simulation_config) - - # Perform greedy optimization to assign branches to wires - decide_guide(energy_matrix, branches, trellis_support, simulation_config) - - # Update guide targets for all assigned branches - for branch in branches: - branch.update_guide(branch.tying.guide_target) - - # Execute tying operations in the L-System string - while tie(lstring, simulation_config): - pass # Continue until no more tying operations are possible - - # Check if this is also a pruning iteration - if current_iteration % pruning_interval_iterations == 0: - # Prune branches until no more can be pruned - while prune(lstring, simulation_config): - pass - - return lstring - - - -branch_hierarchy = {} -branch_hierarchy[main_trunk.name] = [] -enable_color_labeling = simulation_config.label -# ============================================================================= -# L-SYSTEM GRAMMAR DEFINITION -# ============================================================================= -# This section defines the formal grammar for the Envy tree growth simulation. -# The L-System uses modules (symbols) to represent different plant components -# and their growth behaviors. - -# ----------------------------------------------------------------------------- -# MODULE DECLARATIONS -# ----------------------------------------------------------------------------- -# Define the vocabulary of symbols used in the L-System grammar. -# Each module represents a different type of plant component or operation. - -module Attractors # Trellis support structure that guides branch growth -module grow_object # Growing plant parts (trunk, branches, spurs) with length/thickness -module bud # Dormant buds that can break to produce new branches -module branch # Branch segments in the L-System string -module WoodStart # Starting point of wood segments (used for tying operations) - -# ----------------------------------------------------------------------------- -# GLOBAL L-SYSTEM PARAMETERS -# ----------------------------------------------------------------------------- -# Create a growth guide curve for the initial trunk development -trunk_guide_curve = create_bezier_curve(x_range = (-1, 1), y_range = (-1, 1), z_range = (0, 10), seed_value=time.time()) - -# ----------------------------------------------------------------------------- -# AXIOM (STARTING STRING) -# ----------------------------------------------------------------------------- -# The initial L-System string that begins the simulation. -# Starts with the trellis attractors, sets up the trunk guide curve, -# and initializes the trunk growth with proper orientation. -Axiom: Attractors(trellis_support)SetGuide(trunk_guide_curve, main_trunk.growth.max_length)[@GcGetPos(main_trunk.location.start)WoodStart(ParameterSet(type = main_trunk))grow_object(main_trunk)GetPos(main_trunk.location.end)@Ge] - -# ----------------------------------------------------------------------------- -# DERIVATION PARAMETERS -# ----------------------------------------------------------------------------- -# Set the maximum number of derivation steps for the L-System -derivation length: simulation_config.derivation_length - -# ----------------------------------------------------------------------------- -# PRODUCTION RULES -# ----------------------------------------------------------------------------- -# Define how each module type evolves during each derivation step. -# These rules control the growth, branching, and development of the tree. - -production: - -# GROW_OBJECT PRODUCTION RULE -# Handles the growth of plant segments (trunk, branches, spurs) -# Determines whether to continue growing, stop, or produce buds -grow_object(plant_segment) : - if plant_segment == None: - # Null object - terminate this branch - produce * - if plant_segment.length >= plant_segment.growth.max_length: - # Maximum length reached - stop growing this segment - nproduce * - else: - # Continue growing - update segment properties - nproduce SetContour(plant_segment.contour) - #Update internal state of the plant segment - plant_segment.grow_one() - #Update physical representation - if enable_color_labeling: - # Add color visualization if labeling is enabled -- Can this be moved to the start of building the segment? - r, g, b = plant_segment.info.color - nproduce SetColor(r,g,b) - #Produce internode segments (n cylinders per growth step) - n_cylinders = int(plant_segment.growth.growth_length / plant_segment.growth.cylinder_length) - for i in range(n_cylinders): - nproduce I(plant_segment.growth.cylinder_length, plant_segment.growth.thickness, plant_segment) - - #Produce bud (after all cylinders in this growth step) - if plant_segment.pre_bud_rule(plant_segment, simulation_config): - for module in plant_segment.post_bud_rule(plant_segment, simulation_config): - nproduce new(module[0], *module[1]) - - if should_bud(plant_segment, simulation_config): - # Age-based bud production for lateral branching - nproduce bud(ParameterSet(type = plant_segment, num_buds = 0)) - - if plant_segment.post_bud_rule(plant_segment, simulation_config): - for module in plant_segment.post_bud_rule(plant_segment, simulation_config): - nproduce new(module[0], *module[1]) - - produce grow_object(plant_segment) - -# BUD PRODUCTION RULE -# Controls bud break and branch initiation -# Determines when buds activate to produce new branches -bud(bud_parameters) : - if bud_parameters.type.is_bud_break(bud_parameters.num_buds): - # Bud break condition met - create new branch - - new_branch = bud_parameters.type.create_branch() - if new_branch == None: - # Branch creation failed - terminate - produce * - # Register new branch in parent-child relationship tracking - branch_hierarchy[new_branch.name] = [] - branch_hierarchy[bud_parameters.type.name].append(new_branch) - # Update branch counters - bud_parameters.num_buds+=1 - bud_parameters.type.info.num_branches+=1 - - if hasattr(new_branch, 'curve_x_range'): - # Curved branches: set up custom growth guide curve - curve = create_bezier_curve(x_range=new_branch.curve_x_range, y_range=new_branch.curve_y_range, z_range=new_branch.curve_z_range, seed_value=time.time()) - nproduce[@GcSetGuide(curve, new_branch.growth.max_length) - else: - # Straight branches: use default orientation - nproduce [@Gc - # Produce new branch with random orientation and growth object - nproduce @RGetPos(new_branch.location.start)WoodStart(ParameterSet(type = new_branch))/(rd.random()*360)&(rd.random()*360)grow_object(new_branch)GetPos(new_branch.location.end)@Ge]bud(bud_parameters) - - -# ----------------------------------------------------------------------------- -# GEOMETRIC INTERPRETATION (HOMOMORPHISM) -# ----------------------------------------------------------------------------- -# Map abstract L-System modules to concrete 3D geometry for rendering. -# These rules define how the symbolic representation becomes visual. - -homomorphism: - -# Internode segments become cylinders with length and radius -I(a,r,o) --> F(a,r) -# Branch segments also become cylinders -S(a,r,o) --> F(a,r) - -# ----------------------------------------------------------------------------- -# ATTRACTOR VISUALIZATION -# ----------------------------------------------------------------------------- -# Additional production rules for displaying trellis attractor points -production: -Attractors(trellis_support): - # Display enabled attractor points as visual markers - points_to_display = trellis_support.attractor_grid.get_enabled_points() - if len(points_to_display) > 0: - produce [,(3) @g(PointSet(points_to_display,width=simulation_config.attractor_point_width))] +from pathlib import Path +from openalea.lpy import Lsystem +from openalea.lpy import * +from openalea.plantgl.all import * + +BASE_LPY_PATH = Path(__file__).resolve().parents[2] / "base_lpy.lpy" + +vars = { + "prototype_dict_path": "examples.Envy.Envy_prototypes.basicwood_prototypes", + "trunk_class_path": "examples.Envy.Envy_prototypes.Trunk", + "simulation_config_class_path": "examples.Envy.Envy_simulation.EnvySimulationConfig", + "point_generator_path": "examples.Envy.Envy_simulation.generate_points_v_trellis", + "energy_function_path": "examples.Envy.Envy_simulation.get_energy_mat", + "decide_function_path": "examples.Envy.Envy_simulation.decide_guide", + "tie_function_path": "examples.Envy.Envy_simulation.tie", + "prune_function_path": "examples.Envy.Envy_simulation.prune", + "axiom_pitch": 0.0, + "axiom_yaw": 0.0, +} + +lsystem = Lsystem(str(BASE_LPY_PATH), vars) + +for lstring in lsystem: + lsystem.plot(lstring) \ No newline at end of file diff --git a/examples/UFO/UFO.lpy b/examples/UFO/UFO.lpy index 52c6bd8..babd83d 100644 --- a/examples/UFO/UFO.lpy +++ b/examples/UFO/UFO.lpy @@ -1,267 +1,24 @@ -""" -Tying, Pruning and lablelling UFO architecture trees -""" -import sys -sys.path.append('../../') -from stochastic_tree import Support, BasicWood, TreeBranch, BasicWoodConfig -import time -from helper import * -import numpy as np -import random as rd -from examples.UFO.UFO_prototypes import basicwood_prototypes, Trunk -from examples.UFO.UFO_simulation import UFOSimulationConfig, generate_points_ufo, get_energy_mat, decide_guide, tie, prune -from dataclasses import dataclass -import time - -#init -main_trunk = Trunk(copy_from = basicwood_prototypes['trunk']) - -# Create simulation configuration -simulation_config = UFOSimulationConfig() - - - -trellis_support = Support(generate_points_ufo(simulation_config), simulation_config.support_num_wires, simulation_config.support_spacing_wires, simulation_config.support_trunk_wire_point) -tying_interval_iterations = simulation_config.num_iteration_tie -pruning_interval_iterations = simulation_config.num_iteration_prune -main_trunk.tying.guide_target = trellis_support.trunk_wire - -def StartEach(lstring): - """ - Initialize tying updates for trunk and branches at the start of each iteration. - - This function is called at the beginning of each L-System derivation iteration - to prepare the tree structure for potential tying operations. It ensures that - both the trunk and all child branches are ready to participate in the tying - process by updating their tying status. - - The function performs two main tasks: - 1. Updates the trunk's tying status if it has a wire target and hasn't been updated - 2. Updates all child branches' tying status if they haven't been updated - - Args: - lstring: The current L-System string (not used in this function but required - by L-Py's callback interface) - - Returns: - None: Modifies global tree structures in-place - - Note: - This function relies on global variables: branch_hierarchy, trellis_support, main_trunk. - It should be called automatically by L-Py at the start of each iteration. - """ - global branch_hierarchy, trellis_support, main_trunk - - # Update trunk tying status if it has a wire target and needs updating - if trellis_support.trunk_wire and not main_trunk.tying.tie_updated: - main_trunk.tie_update() - - # Update tying status for all child branches that need it - for branch in branch_hierarchy[main_trunk.name]: - if not branch.tying.tie_updated: - branch.tie_update() - - -def EndEach(lstring): - """ - Perform tying and pruning operations at the end of each L-System iteration. - - This function is the main orchestration point for the tree training simulation. - It executes tying operations (assigning branches to trellis wires) and pruning - operations based on configurable iteration intervals. The tying process uses - energy-based optimization to find optimal branch-to-wire assignments. - - The function performs the following sequence when tying is scheduled: - 1. Updates the trunk's guide target (ties trunk first) - 2. Calculates energy matrix for all branch-wire combinations - 3. Uses greedy optimization to assign branches to lowest-energy wires - 4. Updates guide targets for all assigned branches - 5. Performs actual tying operations in the L-System string - 6. Prunes old branches if pruning iteration is reached - - Args: - lstring: The current L-System string containing modules and their parameters - - Returns: - The modified L-System string after tying and pruning operations - - Note: - This function relies on global variables and is called automatically by L-Py - at the end of each derivation iteration. Tying occurs every tying_interval_iterations - iterations, while pruning occurs every pruning_interval_iterations iterations. - """ - global branch_hierarchy, trellis_support, tying_interval_iterations - - current_iteration = getIterationNb() + 1 #GetIterationNb is an L-Py function - - # Check if this is a tying iteration - if current_iteration % tying_interval_iterations == 0: - # Tie trunk to its target wire first (one iteration before branches) - if trellis_support.trunk_wire: - main_trunk.update_guide(main_trunk.tying.guide_target) - - # Calculate energy costs for optimal branch-to-wire assignments - branches = branch_hierarchy[main_trunk.name] - energy_matrix = get_energy_mat(branches, trellis_support, simulation_config) - - # Perform greedy optimization to assign branches to wires - decide_guide(energy_matrix, branches, trellis_support, simulation_config) - - # Update guide targets for all assigned branches - for branch in branches: - branch.update_guide(branch.tying.guide_target) - - # Execute tying operations in the L-System string - while tie(lstring, simulation_config): - pass # Continue until no more tying operations are possible - - # Check if this is also a pruning iteration - if current_iteration % pruning_interval_iterations == 0: - # Prune branches until no more can be pruned - while prune(lstring, simulation_config): - pass - - return lstring - - - -branch_hierarchy = {} -branch_hierarchy[main_trunk.name] = [] -enable_color_labeling = simulation_config.label -# ============================================================================= -# L-SYSTEM GRAMMAR DEFINITION -# ============================================================================= -# This section defines the formal grammar for the UFO tree growth simulation. -# The L-System uses modules (symbols) to represent different plant components -# and their growth behaviors. - -# ----------------------------------------------------------------------------- -# MODULE DECLARATIONS -# ----------------------------------------------------------------------------- -# Define the vocabulary of symbols used in the L-System grammar. -# Each module represents a different type of plant component or operation. - -module Attractors # Trellis support structure that guides branch growth -module grow_object # Growing plant parts (trunk, branches, spurs) with length/thickness -module bud # Dormant buds that can break to produce new branches -module branch # Branch segments in the L-System string -module WoodStart # Starting point of wood segments (used for tying operations) - -# ----------------------------------------------------------------------------- -# GLOBAL L-SYSTEM PARAMETERS -# ----------------------------------------------------------------------------- -# Create a growth guide curve for the initial trunk development -trunk_guide_curve = create_bezier_curve(x_range = (-1, 1), y_range = (-1, 1), z_range = (0, 10), seed_value=time.time()) - -# ----------------------------------------------------------------------------- -# AXIOM (STARTING STRING) -# ----------------------------------------------------------------------------- -# The initial L-System string that begins the simulation. -# Starts with the trellis attractors, sets up the trunk guide curve, -# and initializes the trunk growth with proper orientation. -Axiom: Attractors(trellis_support)SetGuide(trunk_guide_curve, main_trunk.growth.max_length)[@GcGetPos(main_trunk.location.start)WoodStart(ParameterSet(type = main_trunk))&(270)/(0)grow_object(main_trunk)GetPos(main_trunk.location.end)] - -# ----------------------------------------------------------------------------- -# DERIVATION PARAMETERS -# ----------------------------------------------------------------------------- -# Set the maximum number of derivation steps for the L-System -derivation length: simulation_config.derivation_length - -# ----------------------------------------------------------------------------- -# PRODUCTION RULES -# ----------------------------------------------------------------------------- -# Define how each module type evolves during each derivation step. -# These rules control the growth, branching, and development of the tree. - -production: - -# GROW_OBJECT PRODUCTION RULE -# Handles the growth of plant segments (trunk, branches, spurs) -# Determines whether to continue growing, stop, or produce buds -grow_object(plant_segment) : - if plant_segment == None: - # Null object - terminate this branch - produce * - if plant_segment.length >= plant_segment.growth.max_length: - # Maximum length reached - stop growing this segment - nproduce * - else: - # Continue growing - update segment properties - nproduce SetContour(plant_segment.contour) - #Update internal state of the plant segment - plant_segment.grow_one() - #Update physical representation - if enable_color_labeling: - # Add color visualization if labeling is enabled -- Can this be moved to the start of building the segment? - r, g, b = plant_segment.info.color - nproduce SetColor(r,g,b) - #Produce internode segments (n cylinders per growth step) - n_cylinders = int(plant_segment.growth.growth_length / plant_segment.growth.cylinder_length) - for i in range(n_cylinders): - nproduce I(plant_segment.growth.cylinder_length, plant_segment.growth.thickness, plant_segment) - - #Produce bud (after all cylinders in this growth step) - if plant_segment.pre_bud_rule(plant_segment, simulation_config): - for module in plant_segment.post_bud_rule(plant_segment, simulation_config): - nproduce new(module[0], *module[1]) - - if should_bud(plant_segment, simulation_config): - # Age-based bud production for lateral branching - nproduce bud(ParameterSet(type = plant_segment, num_buds = 0)) - - if plant_segment.post_bud_rule(plant_segment, simulation_config): - for module in plant_segment.post_bud_rule(plant_segment, simulation_config): - nproduce new(module[0], *module[1]) - - produce grow_object(plant_segment) - -# BUD PRODUCTION RULE -# Controls bud break and branch initiation -# Determines when buds activate to produce new branches -bud(bud_parameters) : - if bud_parameters.type.is_bud_break(bud_parameters.num_buds): - # Bud break condition met - create new branch - new_branch = bud_parameters.type.create_branch() - if new_branch == None: - # Branch creation failed - terminate - produce * - # Register new branch in parent-child relationship tracking - branch_hierarchy[new_branch.name] = [] - branch_hierarchy[bud_parameters.type.name].append(new_branch) - # Update branch counters - bud_parameters.num_buds+=1 - bud_parameters.type.info.num_branches+=1 - - if hasattr(new_branch, 'curve_x_range'): - # Curved branches: set up custom growth guide curve - curve = create_bezier_curve(x_range=new_branch.curve_x_range, y_range=new_branch.curve_y_range, z_range=new_branch.curve_z_range, seed_value=time.time()) - nproduce[@GcSetGuide(curve, new_branch.growth.max_length) - else: - # Straight branches: use default orientation - nproduce [@Gc - # Produce new branch with random orientation and growth object - nproduce @RGetPos(new_branch.location.start)WoodStart(ParameterSet(type = new_branch))/(rd.random()*360)&(rd.random()*360)grow_object(new_branch)GetPos(new_branch.location.end)@Ge]bud(bud_parameters) - -# ----------------------------------------------------------------------------- -# GEOMETRIC INTERPRETATION (HOMOMORPHISM) -# ----------------------------------------------------------------------------- -# Map abstract L-System modules to concrete 3D geometry for rendering. -# These rules define how the symbolic representation becomes visual. - -homomorphism: - -# Internode segments become cylinders with length and radius -I(a,r,o) --> F(a,r) -# Branch segments also become cylinders -S(a,r,o) --> F(a,r) - -# ----------------------------------------------------------------------------- -# ATTRACTOR VISUALIZATION -# ----------------------------------------------------------------------------- -# Additional production rules for displaying trellis attractor points -production: -Attractors(trellis_support): - # Display enabled attractor points as visual markers - points_to_display = trellis_support.attractor_grid.get_enabled_points() - if len(points_to_display) > 0: - produce [,(3) @g(PointSet(points_to_display,width=simulation_config.attractor_point_width))] +from pathlib import Path +from openalea.lpy import Lsystem +from openalea.lpy import * +from openalea.plantgl.all import * + +BASE_LPY_PATH = Path(__file__).resolve().parents[2] / "base_lpy.lpy" + +vars = { + "prototype_dict_path": "examples.UFO.UFO_prototypes.basicwood_prototypes", + "trunk_class_path": "examples.UFO.UFO_prototypes.Trunk", + "simulation_config_class_path": "examples.UFO.UFO_simulation.UFOSimulationConfig", + "point_generator_path": "examples.UFO.UFO_simulation.generate_points_ufo", + "energy_function_path": "examples.UFO.UFO_simulation.get_energy_mat", + "decide_function_path": "examples.UFO.UFO_simulation.decide_guide", + "tie_function_path": "examples.UFO.UFO_simulation.tie", + "prune_function_path": "examples.UFO.UFO_simulation.prune", + "axiom_pitch": 270.0, + "axiom_yaw": 0.0, +} + +lsystem = Lsystem(str(BASE_LPY_PATH), vars) + +for lstring in lsystem: + lsystem.plot(lstring) diff --git a/helper.py b/helper.py index 72f65e0..5ffefc2 100644 --- a/helper.py +++ b/helper.py @@ -19,6 +19,8 @@ from random import uniform, seed from numpy import linspace, pi, sin, cos import numpy as np +from typing import Callable, Dict, Iterable +import importlib def cut_from(pruning_position, lstring, lsystem_path=None): @@ -236,3 +238,66 @@ def should_bud(plant_segment, simulation_config): """Determine if a plant segment should produce a bud""" return np.isclose(plant_segment.info.age % plant_segment.bud_spacing_age, 0, atol=simulation_config.tolerance) + + +def start_each_common( + lstring, + branch_hierarchy: Dict[str, Iterable], + trellis_support, + main_trunk, +): + """Shared pre-iteration tying preparation logic.""" + del lstring # unused in shared logic; kept for L-Py parity + + if trellis_support.trunk_wire and not main_trunk.tying.tie_updated: + main_trunk.tie_update() + + for branch in branch_hierarchy[main_trunk.name]: + if not branch.tying.tie_updated: + branch.tie_update() + + +def end_each_common( + lstring, + branch_hierarchy: Dict[str, Iterable], + trellis_support, + tying_interval_iterations: int, + pruning_interval_iterations: int, + simulation_config, + main_trunk, + get_iteration_number: Callable[[], int], + get_energy_matrix, + decide_guide_fn, + tie_fn, + prune_fn, +): + """Shared post-iteration tying and pruning orchestration.""" + current_iteration = get_iteration_number() + 1 + + if current_iteration % tying_interval_iterations == 0: + if trellis_support.trunk_wire: + main_trunk.update_guide(main_trunk.tying.guide_target) + + branches = branch_hierarchy[main_trunk.name] + energy_matrix = get_energy_matrix(branches, trellis_support, simulation_config) + + decide_guide_fn(energy_matrix, branches, trellis_support, simulation_config) + + for branch in branches: + branch.update_guide(branch.tying.guide_target) + + while tie_fn(lstring, simulation_config): + pass + + if current_iteration % pruning_interval_iterations == 0: + while prune_fn(lstring, simulation_config): + pass + + return lstring + + +def resolve_attr(path: str): + """Import a fully qualified attribute path.""" + pkg_path, attr_name = path.rsplit('.', 1) + pkg = importlib.import_module(pkg_path) + return getattr(pkg, attr_name) From 2f097454734caad2de540738b1357bda0570b3df Mon Sep 17 00:00:00 2001 From: Abhinav Jain Date: Tue, 18 Nov 2025 16:30:21 -0800 Subject: [PATCH 13/14] Add color mapping and fix make_n_trees.py --- __init__.py | 5 ++ base_lpy.lpy | 84 ++++++++++++++------- color_manager.py | 53 +++++++++++++ examples/Envy/Envy.lpy | 14 ++-- examples/Envy/Envy_simulation.py | 32 +------- examples/UFO/UFO.lpy | 12 +-- examples/UFO/UFO_simulation.py | 31 +------- simulation_base.py | 23 +++++- stochastic_tree.py | 11 ++- tree_generation/make_n_trees.py | 125 +++++++++++++++++++++++-------- 10 files changed, 256 insertions(+), 134 deletions(-) create mode 100644 color_manager.py diff --git a/__init__.py b/__init__.py index e69de29..ca96213 100644 --- a/__init__.py +++ b/__init__.py @@ -0,0 +1,5 @@ +"""LPY Tree Simulation package.""" + +from .color_manager import ColorManager + +__all__ = ["ColorManager"] diff --git a/base_lpy.lpy b/base_lpy.lpy index 849baa1..51a226d 100644 --- a/base_lpy.lpy +++ b/base_lpy.lpy @@ -23,35 +23,27 @@ from dataclasses import dataclass # Override these externs via Lsystem(..., variables) to point at a specific # architecture without editing this shared file. extern( + color_manager = None, prototype_dict_path = "examples.Envy.Envy_prototypes.basicwood_prototypes", trunk_class_path = "examples.Envy.Envy_prototypes.Trunk", simulation_config_class_path = "examples.Envy.Envy_simulation.EnvySimulationConfig", - point_generator_path = "examples.Envy.Envy_simulation.generate_points_v_trellis", - energy_function_path = "examples.Envy.Envy_simulation.get_energy_mat", - decide_function_path = "examples.Envy.Envy_simulation.decide_guide", - tie_function_path = "examples.Envy.Envy_simulation.tie", - prune_function_path = "examples.Envy.Envy_simulation.prune", + simulation_class_path = "examples.Envy.Envy_simulation.EnvySimulation", axiom_pitch = 0.0, axiom_yaw = 0.0, -) - -# Resolve extern-provided dotted paths so the rest of the script can operate on +)# Resolve extern-provided dotted paths so the rest of the script can operate on # concrete classes/functions exactly like the Envy/UFO drivers do. basicwood_prototypes = resolve_attr(prototype_dict_path) Trunk = resolve_attr(trunk_class_path) SimulationConfigClass = resolve_attr(simulation_config_class_path) -point_generator_fn = resolve_attr(point_generator_path) -get_energy_mat = resolve_attr(energy_function_path) -decide_guide = resolve_attr(decide_function_path) -tie = resolve_attr(tie_function_path) -prune = resolve_attr(prune_function_path) +SimulationClass = resolve_attr(simulation_class_path) simulation_config = SimulationConfigClass() +simulation = SimulationClass(simulation_config) main_trunk = Trunk(copy_from = basicwood_prototypes['trunk']) # Build trellis geometry and cadence numbers up front so callbacks stay simple. trellis_support = Support( - point_generator_fn(simulation_config), + simulation.generate_points(), simulation_config.support_num_wires, simulation_config.support_spacing_wires, simulation_config.support_trunk_wire_point, @@ -62,7 +54,30 @@ main_trunk.tying.guide_target = trellis_support.trunk_wire # Track parent/child relationships for tying/pruning decisions. branch_hierarchy = {main_trunk.name: []} -enable_color_labeling = simulation_config.label +enable_semantic_labeling = simulation_config.semantic_label +enable_instance_labeling = simulation_config.instance_label +enable_per_cylinder_labeling = simulation_config.per_cylinder_label + +if (enable_instance_labeling or enable_per_cylinder_labeling) and color_manager is None: + raise ValueError( + "Instance labeling is enabled but no color_manager extern was provided." + ) + + +def _get_energy_mat(branches, arch, _config): + return simulation.get_energy_mat(branches, arch) + + +def _decide_guide(energy_matrix, branches, arch, _config): + return simulation.decide_guide(energy_matrix, branches, arch) + + +def _tie(lstring, _config): + return simulation.tie(lstring) + + +def _prune(lstring, _config): + return simulation.prune(lstring) # L-Py callbacks delegate to the Python helpers so architectures share logic. def StartEach(lstring): @@ -81,12 +96,12 @@ def EndEach(lstring): simulation_config, main_trunk, getIterationNb, - get_energy_mat, - decide_guide, - tie, - prune, + _get_energy_mat, + _decide_guide, + _tie, + _prune, ) - +generalized_cylinder = getattr(simulation_config, "use_generalized_cylinder", False) # ============================================================================= # L-SYSTEM GRAMMAR DEFINITION # ============================================================================= @@ -149,18 +164,28 @@ grow_object(plant_segment) : #Update internal state of the plant segment plant_segment.grow_one() #Update physical representation - if enable_color_labeling: + if enable_semantic_labeling: # Add color visualization if labeling is enabled -- Can this be moved to the start of building the segment? r, g, b = plant_segment.info.color + plant_part = plant_segment.name.split("_")[0] # Get the part type (e.g., "trunk", "branch") + color_manager.set_unique_color((r,g,b), plant_part) # Ensure part type has a color assig + nproduce SetColor(r,g,b) + if enable_instance_labeling: + # Instance-level labeling (unique color per branch instance) + r, g, b = color_manager.get_unique_color(plant_segment.name) nproduce SetColor(r,g,b) + #Produce internode segments (n cylinders per growth step) n_cylinders = int(plant_segment.growth.growth_length / plant_segment.growth.cylinder_length) for i in range(n_cylinders): + if enable_per_cylinder_labeling: + r, g, b = color_manager.get_unique_color(plant_segment.name, if_exists=False) + nproduce SetColor(r,g,b) nproduce I(plant_segment.growth.cylinder_length, plant_segment.growth.thickness, plant_segment) #Produce bud (after all cylinders in this growth step) if plant_segment.pre_bud_rule(plant_segment, simulation_config): for generated in plant_segment.post_bud_rule(plant_segment, simulation_config): - nproduce new(generated[0], *generated[1]) + nproduce new(generated[0], *generated[1]) if should_bud(plant_segment, simulation_config): # Age-based bud production for lateral branching @@ -190,15 +215,18 @@ bud(bud_parameters) : bud_parameters.num_buds+=1 bud_parameters.type.info.num_branches+=1 + nproduce [ + if generalized_cylinder: + nproduce @Gc if hasattr(new_branch, 'curve_x_range'): # Curved branches: set up custom growth guide curve - curve = create_bezier_curve(x_range=new_branch.curve_x_range, y_range=new_branch.curve_y_range, z_range=new_branch.curve_z_range, seed_value=time.time()) - nproduce[@GcSetGuide(curve, new_branch.growth.max_length) - else: - # Straight branches: use default orientation - nproduce [@Gc + curve = create_bezier_curve(x_range=new_branch.curve_x_range, y_range=new_branch.curve_y_range, z_range=new_branch.curve_z_range, seed_value=rd.randint(0,1000)) + nproduceSetGuide(curve, new_branch.growth.max_length) # Produce new branch with random orientation and growth object - nproduce @RGetPos(new_branch.location.start)WoodStart(ParameterSet(type = new_branch))/(rd.random()*360)&(rd.random()*360)grow_object(new_branch)GetPos(new_branch.location.end)@Ge]bud(bud_parameters) + nproduce @RGetPos(new_branch.location.start)WoodStart(ParameterSet(type = new_branch))/(rd.random()*360)&(rd.random()*360)grow_object(new_branch)GetPos(new_branch.location.end) + if generalized_cylinder: + nproduce @Ge + produce ]bud(bud_parameters) # ----------------------------------------------------------------------------- # GEOMETRIC INTERPRETATION (HOMOMORPHISM) diff --git a/color_manager.py b/color_manager.py new file mode 100644 index 0000000..6662653 --- /dev/null +++ b/color_manager.py @@ -0,0 +1,53 @@ +""" +Color management utilities for L-Py tree simulation system. + +This module provides utilities for assigning unique colors to tree segments +for visualization and labeling purposes. +""" + +import itertools +import json + + +class ColorManager: + """Manages assignment of unique colors to named entities.""" + + def __init__(self): + self.color_to_name = {} + self.name_to_color = {} + # Permute all possible colors to make a list + self.all_colors = list(itertools.product(range(256), repeat=3)) # 0-255 inclusive + self.color_pointer = 0 + + def get_unique_color(self, name, if_exists=True): + """Get a unique RGB color tuple for the given name.""" + if name in self.name_to_color and if_exists: + return self.name_to_color[name] + + if self.color_pointer >= len(self.all_colors): + raise ValueError("Ran out of unique colors!") + + unique_color = self.all_colors[self.color_pointer] + self.color_pointer += 1 + + # Assign and save mapping + self.name_to_color[name] = unique_color + self.color_to_name[unique_color] = name + + return unique_color + + def export_mapping(self, filename): + """Export color -> name mapping to JSON""" + export_dict = {} + for color, name in self.color_to_name.items(): + export_dict[str(color)] = name + + with open(filename, "w") as f: + json.dump(export_dict, f, indent=4) + + print(f"Exported color mappings to {filename}") + + def set_unique_color(self, color, name): + """Set a specific color for a given name.""" + self.name_to_color[name] = color + self.color_to_name[color] = name \ No newline at end of file diff --git a/examples/Envy/Envy.lpy b/examples/Envy/Envy.lpy index dbf3aee..8c5e2d8 100644 --- a/examples/Envy/Envy.lpy +++ b/examples/Envy/Envy.lpy @@ -2,18 +2,18 @@ from pathlib import Path from openalea.lpy import Lsystem from openalea.lpy import * from openalea.plantgl.all import * +from lpy_treesim import ColorManager BASE_LPY_PATH = Path(__file__).resolve().parents[2] / "base_lpy.lpy" +color_manager = ColorManager() + vars = { "prototype_dict_path": "examples.Envy.Envy_prototypes.basicwood_prototypes", "trunk_class_path": "examples.Envy.Envy_prototypes.Trunk", "simulation_config_class_path": "examples.Envy.Envy_simulation.EnvySimulationConfig", - "point_generator_path": "examples.Envy.Envy_simulation.generate_points_v_trellis", - "energy_function_path": "examples.Envy.Envy_simulation.get_energy_mat", - "decide_function_path": "examples.Envy.Envy_simulation.decide_guide", - "tie_function_path": "examples.Envy.Envy_simulation.tie", - "prune_function_path": "examples.Envy.Envy_simulation.prune", + "simulation_class_path": "examples.Envy.Envy_simulation.EnvySimulation", + "color_manager": color_manager, "axiom_pitch": 0.0, "axiom_yaw": 0.0, } @@ -21,4 +21,6 @@ vars = { lsystem = Lsystem(str(BASE_LPY_PATH), vars) for lstring in lsystem: - lsystem.plot(lstring) \ No newline at end of file + lsystem.plot(lstring) + +# color_manager.export_mapping("envy_color_mapping.json") \ No newline at end of file diff --git a/examples/Envy/Envy_simulation.py b/examples/Envy/Envy_simulation.py index 03424fa..bb8a0d3 100644 --- a/examples/Envy/Envy_simulation.py +++ b/examples/Envy/Envy_simulation.py @@ -25,8 +25,9 @@ class EnvySimulationConfig(SimulationConfig): trellis_z_spacing: float = 0.45 # Envy-specific Growth Parameters - growth_length: float = 0.1 - bud_spacing_age: int = 2 + semantic_label: bool = True + instance_label: bool = False + per_cylinder_label: bool = False class EnvySimulation(TreeSimulationBase): @@ -60,31 +61,4 @@ def generate_points(self): pts.append((x[i], y[i], z[i])) return pts - -# Backwards compatibility: provide standalone functions that use the class -def generate_points_v_trellis(simulation_config): - """Backward compatibility wrapper for generate_points.""" - sim = EnvySimulation(simulation_config) - return sim.generate_points() - -def get_energy_mat(branches, arch, simulation_config): - """Backward compatibility wrapper for get_energy_mat.""" - sim = EnvySimulation(simulation_config) - return sim.get_energy_mat(branches, arch) - -def decide_guide(energy_matrix, branches, arch, simulation_config): - """Backward compatibility wrapper for decide_guide.""" - sim = EnvySimulation(simulation_config) - return sim.decide_guide(energy_matrix, branches, arch) - -def prune(lstring, simulation_config): - """Backward compatibility wrapper for prune.""" - sim = EnvySimulation(simulation_config) - return sim.prune(lstring) - -def tie(lstring, simulation_config): - """Backward compatibility wrapper for tie.""" - sim = EnvySimulation(simulation_config) - return sim.tie(lstring) - diff --git a/examples/UFO/UFO.lpy b/examples/UFO/UFO.lpy index babd83d..dfc4fe9 100644 --- a/examples/UFO/UFO.lpy +++ b/examples/UFO/UFO.lpy @@ -2,18 +2,18 @@ from pathlib import Path from openalea.lpy import Lsystem from openalea.lpy import * from openalea.plantgl.all import * +from lpy_treesim import ColorManager BASE_LPY_PATH = Path(__file__).resolve().parents[2] / "base_lpy.lpy" +color_manager = ColorManager() + vars = { "prototype_dict_path": "examples.UFO.UFO_prototypes.basicwood_prototypes", "trunk_class_path": "examples.UFO.UFO_prototypes.Trunk", "simulation_config_class_path": "examples.UFO.UFO_simulation.UFOSimulationConfig", - "point_generator_path": "examples.UFO.UFO_simulation.generate_points_ufo", - "energy_function_path": "examples.UFO.UFO_simulation.get_energy_mat", - "decide_function_path": "examples.UFO.UFO_simulation.decide_guide", - "tie_function_path": "examples.UFO.UFO_simulation.tie", - "prune_function_path": "examples.UFO.UFO_simulation.prune", + "simulation_class_path": "examples.UFO.UFO_simulation.UFOSimulation", + "color_manager": color_manager, "axiom_pitch": 270.0, "axiom_yaw": 0.0, } @@ -22,3 +22,5 @@ lsystem = Lsystem(str(BASE_LPY_PATH), vars) for lstring in lsystem: lsystem.plot(lstring) + +# color_manager.export_mapping("ufo_color_mapping.json") diff --git a/examples/UFO/UFO_simulation.py b/examples/UFO/UFO_simulation.py index 1fdcf32..0578159 100644 --- a/examples/UFO/UFO_simulation.py +++ b/examples/UFO/UFO_simulation.py @@ -26,6 +26,9 @@ class UFOSimulationConfig(SimulationConfig): # UFO-specific Growth Parameters thickness_multiplier: float = 1.2 # Multiplier for internode thickness + semantic_label: bool = True + instance_label: bool = False + per_cylinder_label: bool = False class UFOSimulation(TreeSimulationBase): @@ -61,31 +64,3 @@ def generate_points(self): wire_attachment_points.append((x[point_index], y[point_index], z[point_index])) return wire_attachment_points - - -# Backwards compatibility: provide standalone functions that use the class -# Backwards compatibility: provide standalone functions that use the class -def generate_points_ufo(simulation_config): - """Backward compatibility wrapper for generate_points.""" - sim = UFOSimulation(simulation_config) - return sim.generate_points() - -def get_energy_mat(branches, arch, simulation_config): - """Backward compatibility wrapper for get_energy_mat.""" - sim = UFOSimulation(simulation_config) - return sim.get_energy_mat(branches, arch) - -def decide_guide(energy_matrix, branches, arch, simulation_config): - """Backward compatibility wrapper for decide_guide.""" - sim = UFOSimulation(simulation_config) - return sim.decide_guide(energy_matrix, branches, arch) - -def prune(lstring, simulation_config): - """Backward compatibility wrapper for prune.""" - sim = UFOSimulation(simulation_config) - return sim.prune(lstring) - -def tie(lstring, simulation_config): - """Backward compatibility wrapper for tie.""" - sim = UFOSimulation(simulation_config) - return sim.tie(lstring) diff --git a/simulation_base.py b/simulation_base.py index 9e94c58..e423aff 100644 --- a/simulation_base.py +++ b/simulation_base.py @@ -28,9 +28,10 @@ class SimulationConfig(ABC): num_iteration_tie: int = 5 num_iteration_prune: int = 16 - # Display Options - label: bool = True - + # Label Options + semantic_label: bool = False + instance_label: bool = True + per_cylinder_label: bool = False # Support Structure support_num_wires: int = 14 support_spacing_wires: int = 1 @@ -45,13 +46,27 @@ class SimulationConfig(ABC): # L-System Parameters derivation_length: int = 128 # Number of derivation steps + use_generalized_cylinder: bool = False # Whether to wrap new branches in @Gc/@Ge blocks # Growth Parameters - tolerance: float = 0.01 # Tolerance for age-based bud spacing (bud_age_tolerance) + tolerance: float = 1e-5 # Tolerance for comparison between floats # Visualization Parameters attractor_point_width: int = 10 # Width of attractor points in visualization + def __post_init__(self): + """Make sure only one labeling option is enabled at a time.""" + label_options = [ + self.semantic_label, + self.instance_label, + self.per_cylinder_label + ] + if sum(label_options) > 1: + raise ValueError( + "Only one of semantic_label, instance_label, or per_cylinder_label " + "can be True at a time." + ) + class TreeSimulationBase(ABC): """ diff --git a/stochastic_tree.py b/stochastic_tree.py index 37e025d..835fecd 100644 --- a/stochastic_tree.py +++ b/stochastic_tree.py @@ -63,7 +63,7 @@ class InfoState: prunable: bool = True order: int = 0 num_branches: int = 0 - color: int = 0 + color: tuple = (0, 0, 0) # RGB tuple for visualization material: int = 0 branch_dict: any = None # collections.deque @@ -96,8 +96,13 @@ class BasicWoodConfig: curve_z_range: tuple = (-1, 1) # Z bounds for Bezier curve control points def __post_init__(self): - """Initialize mutable defaults if needed.""" - pass + """Validate geometric parameters for consistent growth behavior.""" + if self.growth_length is not None and self.cylinder_length is not None: + if self.growth_length < self.cylinder_length: + raise ValueError( + "BasicWoodConfig.growth_length must be >= cylinder_length " + f"(got {self.growth_length} < {self.cylinder_length})" + ) class BasicWood(ABC): @staticmethod diff --git a/tree_generation/make_n_trees.py b/tree_generation/make_n_trees.py index 0cc9e37..7e13f29 100644 --- a/tree_generation/make_n_trees.py +++ b/tree_generation/make_n_trees.py @@ -1,42 +1,105 @@ +import argparse +from dataclasses import dataclass import os +from pathlib import Path +import random import sys -sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../../'))) -sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../'))) -import random as rd + from openalea.lpy import Lsystem + +from lpy_treesim import ColorManager from lpy_treesim.tree_generation.helpers import write -import argparse -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument('--num_trees', type=int, default=1) - parser.add_argument('--output_dir', type=str, default='dataset/') - parser.add_argument('--lpy_file', type=str, default='examples/UFO/UFO.lpy') - parser.add_argument('--verbose', action='store_true', default=False) + +BASE_LPY_PATH = Path(__file__).resolve().parents[1] / "base_lpy.lpy" + +# Ensure repository root is discoverable for prototype imports +sys.path.insert(0, str(BASE_LPY_PATH.parents[0])) + + +MAX_TREES = 99_999 + + +@dataclass +class TreeNamingConfig: + namespace: str + tree_type: str + + def _prefix(self, index: int) -> str: + if index > MAX_TREES: + raise ValueError(f"Tree index {index} exceeds maximum supported value {MAX_TREES}.") + return f"{self.namespace}_{self.tree_type}_{index:05d}" + + def mesh_filename(self, index: int) -> str: + return f"{self._prefix(index)}.ply" + + def color_map_filename(self, index: int) -> str: + return f"{self._prefix(index)}_colors.json" + + +def build_lsystem(tree_name: str) -> tuple[Lsystem, ColorManager]: + color_manager = ColorManager() + extern_vars = { + "prototype_dict_path": f"examples.{tree_name}.{tree_name}_prototypes.basicwood_prototypes", + "trunk_class_path": f"examples.{tree_name}.{tree_name}_prototypes.Trunk", + "simulation_config_class_path": f"examples.{tree_name}.{tree_name}_simulation.{tree_name}SimulationConfig", + "simulation_class_path": f"examples.{tree_name}.{tree_name}_simulation.{tree_name}Simulation", + "color_manager": color_manager, + "axiom_pitch": 0.0, + "axiom_yaw": 0.0, + } + lsystem = Lsystem(str(BASE_LPY_PATH), extern_vars) + return lsystem, color_manager + + +def generate_tree(lsystem: Lsystem, rng_seed: int, verbose: bool): + random.seed(rng_seed) + if verbose: + print(f"INFO: RNG seed {rng_seed}") + lstring = lsystem.axiom + for iteration in range(lsystem.derivationLength): + lstring = lsystem.derive(lstring, iteration, 1) + lsystem.plot(lstring) + return lstring, lsystem.sceneInterpretation(lstring) + + +def ensure_output_dir(path: Path): + path.mkdir(parents=True, exist_ok=True) + + +def main(): + parser = argparse.ArgumentParser(description="Generate and save multiple L-Py trees.") + parser.add_argument('--num_trees', type=int, default=1, help='Number of trees to generate') + parser.add_argument('--output_dir', type=Path, default=Path('dataset/'), help='Directory for outputs') + parser.add_argument('--tree_name', type=str, default='UFO', help='Tree family to generate (UFO/Envy/etc.)') + parser.add_argument('--verbose', action='store_true', help='Print progress details') + parser.add_argument('--rng-seed', type=int, default=None, help='Optional deterministic seed') + parser.add_argument('--namespace', type=str, default='lpy', help='Prefix namespace for output filenames') args = parser.parse_args() - num_trees = args.num_trees - output_dir = args.output_dir - lpy_file = args.lpy_file - for i in range(num_trees): + if args.num_trees > (MAX_TREES + 1): + raise ValueError(f"num_trees={args.num_trees} exceeds supported maximum of {MAX_TREES + 1}.") + + naming = TreeNamingConfig(namespace=args.namespace, tree_type=args.tree_name) + ensure_output_dir(args.output_dir) + + rng = random.Random(args.rng_seed) + for index in range(args.num_trees): + lsystem, color_manager = build_lsystem(args.tree_name) + seed_value = rng.randint(0, 1_000_000) if args.verbose: - print("INFO: Generating tree number: ", i) - rand_seed = rd.randint(0,1000) - variables = {'label': True, 'seed_val': rand_seed} - l = Lsystem(lpy_file, variables) - lstring = l.axiom - for time in range(l.derivationLength): - lstring = l.derive(lstring, time, 1) - l.plot(lstring) - # l.plot() - scene = l.sceneInterpretation(lstring) - # scene.save("{}/tree_{}.lpy".format(output_dir, i)) - if not os.path.exists(output_dir): - os.makedirs(output_dir) + print(f"INFO: Generating {args.tree_name} tree #{index:03d}") + lstring, scene = generate_tree(lsystem, seed_value, args.verbose) + mesh_path = args.output_dir / naming.mesh_filename(index) + color_path = args.output_dir / naming.color_map_filename(index) + write(str(mesh_path), scene) + color_manager.export_mapping(str(color_path)) if args.verbose: - print("INFO: Writing tree number: ", i) - # scene.save("{}/tree_{}.obj".format(output_dir, i)) - write("{}/tree_{}.ply".format(output_dir, i), scene) + print(f"INFO: Wrote {mesh_path} and {color_path}") del scene del lstring - del l + del lsystem + + +if __name__ == "__main__": + main() From 3872c4e923e6d51601bb9560d8b5d8beee285d03 Mon Sep 17 00:00:00 2001 From: Abhinav Jain Date: Tue, 18 Nov 2025 16:42:23 -0800 Subject: [PATCH 14/14] Updated document --- docs/make.bat | 26 ++++ docs/source/files.rst | 91 ++++++++----- docs/source/index.rst | 10 ++ docs/source/installation.rst | 74 +++++------ docs/source/methods.rst | 78 ++++++++--- docs/source/resources.rst | 32 +++-- docs/source/usage.rst | 250 +++++++++++++++++++++++++++++------ 7 files changed, 417 insertions(+), 144 deletions(-) create mode 100644 docs/make.bat diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..26aeec1 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,26 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) + +set BUILDDIR=_build +set SOURCEDIR=source + + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 "%SOURCEDIR%" "%BUILDDIR%" %SPHINXOPTS% %O% + +popd +goto end + +:help +%SPHINXBUILD% -M help "%SOURCEDIR%" "%BUILDDIR%" %SPHINXOPTS% %O% + +popd +:end diff --git a/docs/source/files.rst b/docs/source/files.rst index d83e1e2..48468f0 100644 --- a/docs/source/files.rst +++ b/docs/source/files.rst @@ -1,51 +1,74 @@ Files and Directory Structure ============================= -This document provides an overview of the important files and directories in the `lpy_treesim` project. +The project is organized so that every custom tree lives wholly inside the +`examples/` package, while the shared runtime sits at the repository root. -Top-Level Files ---------------- +Top-level Python modules +------------------------ -- **`README.rst`**: The main README file for the project. -- **`helper.py`**: Contains helper functions used by other scripts in the project. -- **`stochastic_tree.py`**: The core module for generating stochastic trees. -- **`.gitignore`**: A file that specifies which files and directories to ignore in a Git repository. +``base_lpy.lpy`` + The canonical L-Py grammar. It loads extern variables that point to your + prototype dictionary, simulation classes, and color manager, then drives the + derivation/prune/tie loop. -`docs/` -------- +``simulation_base.py`` + Defines `SimulationConfig` and `TreeSimulationBase`. All simulation files in + `examples/` inherit from these classes. -This directory contains the documentation for the project. +``stochastic_tree.py`` + Hosts the `TreeBranch`, `BasicWood`, `Support`, and related data structures + referenced by prototypes. -- **`Makefile`**: A makefile with commands to build the documentation. -- **`source/`**: The source files for the documentation, written in reStructuredText. - - **`conf.py`**: The configuration file for Sphinx, the documentation generator. - - **`index.rst`**: The main entry point for the documentation. - - **`installation.rst`**: Instructions on how to install the project. - - **`usage.rst`**: An explanation of how to use the project. - - **`files.rst`**: An overview of the important files and directories in the project. - - **`resources.rst`**: A list of resources related to the project. - - **`methods.rst`**: A description of the methods used in the project. -- **`_static/`**: Static files, such as images and videos, that are used in the documentation. +``color_manager.py`` + Implements `ColorManager`, which tracks per-instance color IDs and can dump + them via `export_mapping` to JSON. -`examples/` ------------ +Documentation (`docs/`) +----------------------- -This directory contains example `.lpy` files that demonstrate how to use `lpy_treesim`. +Sphinx project containing the pages you are reading now. Run `make html` inside +`lpy_treesim/docs` to build the site locally. -- **`legacy/`**: Older example files. -- **`UFO/`**: Examples related to the UFO cherry tree architecture. +Examples (`examples/`) +---------------------- -`modules_test/` ---------------- +Each subfolder describes one tree training system. For `UFO` you will find: -This directory contains test files for the modules in the project. The files in this folder use classes and functions defined in `stochastic_tree.py` and can be a good example of how to use the `BasicWood`, `Wire`, and `Support` classes. +``examples/UFO/UFO_prototypes.py`` + Prototype classes (`Trunk`, `Branch`, `Spur`, etc.) plus the + `basicwood_prototypes` dictionary. -`other_files/` --------------- +``examples/UFO/UFO_simulation.py`` + `UFOSimulationConfig` and `UFOSimulation`, consumed by the base grammar. -These files may or may not work. They were used in previous iterations of `lpy_treesim` and are kept for reference. +``examples/UFO/UFO.lpy`` + Optional GUI entry point if you want to run the species manually inside + L-Py. Most development happens via `base_lpy.lpy`, but the standalone files + are helpful for debugging. -`tree_generation/` ------------------- +Add a new tree type by duplicating this directory and renaming files to match +your `--tree_name` argument. -This directory contains scripts for generating and converting tree models. \ No newline at end of file +Tree generation utilities (`tree_generation/`) +--------------------------------------------- + +``tree_generation/make_n_trees.py`` + CLI entry point that imports the base grammar, wires in your prototypes, and + exports `.ply` meshes plus `{...}_colors.json` label maps. Accepts + `--num_trees`, `--namespace`, `--rng-seed`, and `--output_dir`. + +``tree_generation/helpers.py`` + Contains `write` (PLY serializer) and other small utilities referenced by + the generator. + +Supporting assets +----------------- + +``dataset/`` + Destination for generated `.ply` files by default. Subfolders such as + `test_output/` are safe to delete or replace with your own datasets. + +``media/`` and ``other_files/`` + Legacy L-Py grammars, renderings, and experiments kept for historical + reference. \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst index fc31c7f..445697d 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -6,6 +6,16 @@ Welcome to lpy_treesim's documentation! ======================================= +`lpy_treesim` bundles a reusable L-Py grammar (`base_lpy.lpy`), +prototype definitions, and automation scripts so you can describe a new tree +architecture and batch-generate thousands of 3D assets. These docs walk +through the entire workflow: + +* set up the environment and dependencies, +* design prototypes plus simulation parameters, +* run the `make_n_trees.py` generator with deterministic naming, and +* understand how the supporting modules fit together. + .. toctree:: :maxdepth: 2 :caption: Contents: diff --git a/docs/source/installation.rst b/docs/source/installation.rst index 51d5b21..d2bca7e 100644 --- a/docs/source/installation.rst +++ b/docs/source/installation.rst @@ -1,65 +1,63 @@ Installation -============== +============ -This document provides instructions on how to install `lpy_treesim` and its dependencies. +`lpy_treesim` ships as a Python package plus a collection of L-Py grammars, so +you need both the OpenAlea/L-Py toolchain and the Python modules in this repo. Prerequisites ------------- -- **Python 3.x**: Make sure you have a working Python 3 installation. -- **Conda**: `lpy_treesim` relies on the Conda package manager to handle its environment and dependencies. If you don't have Conda, you can install it by following the official documentation: https://docs.conda.io/projects/conda/en/latest/user-guide/install/ +- **Conda (recommended)** for installing `openalea.lpy` and PlantGL. +- **Python 3.9+** for running the helper scripts. +- A GPU is not required; everything runs on CPU. -Installing L-Py ---------------- +Set up the L-Py environment +--------------------------- -`lpy_treesim` is built on top of L-Py, a Python-based L-system simulator. To install L-Py, follow these steps: +1. Create a dedicated environment that contains L-Py and PlantGL: -1. **Create a Conda Environment**: Open your terminal and create a new Conda environment named `lpy`: + .. code-block:: bash - .. code-block:: bash + conda create -n lpy openalea.lpy plantgl python=3.9 -c fredboudon -c conda-forge - conda create -n lpy openalea.lpy -c fredboudon -c conda-forge +2. Activate the environment any time you work on the project: -2. **Activate the Environment**: Activate the newly created environment: + .. code-block:: bash - .. code-block:: bash + conda activate lpy - conda activate lpy +3. Validate the installation by launching the GUI (optional but handy for + debugging grammars): -3. **Run L-Py**: You can now run L-Py to ensure it's installed correctly: + .. code-block:: bash - .. code-block:: bash + lpy - lpy +Install `lpy_treesim` +--------------------- -For more detailed information and troubleshooting, refer to the official L-Py documentation: https://lpy.readthedocs.io/en/latest/user/installing.html +With the environment active, clone and install the package in editable mode so +that L-Py can import your custom prototypes: -Installing `lpy_treesim` ------------------------- - -Once you have L-Py set up, you can install `lpy_treesim`: - -1. **Clone the Repository**: Clone this repository to your local machine: - - .. code-block:: bash - - git clone https://github.com/your-username/lpy_treesim.git - cd lpy_treesim - -2. **Install Dependencies**: The required Python packages are listed in the `requirements.txt` file. You can install them using pip: +.. code-block:: bash - .. code-block:: bash + git clone https://github.com/OSUrobotics/lpy_treesim.git + cd lpy_treesim + pip install -e . - pip install -r requirements.txt +Editable installs expose modules such as `lpy_treesim.ColorManager` and ensure +`examples/` can import the shared base grammar. -Running the Examples --------------------- +Optional tooling +----------------- -The `examples` directory contains several examples that demonstrate how to use `lpy_treesim`. To run an example, navigate to the `examples` directory and run the desired script: +The repository includes a Sphinx documentation project. To build the docs +locally install Sphinx, then run `make`: .. code-block:: bash - cd examples - python example_script.py + cd lpy_treesim/lpy_treesim/docs + pip install sphinx + make html -Replace `example_script.py` with the actual name of the example you want to run. \ No newline at end of file +Open `_build/html/index.html` in a browser to preview the rendered docs. \ No newline at end of file diff --git a/docs/source/methods.rst b/docs/source/methods.rst index 75c7223..c6503f8 100644 --- a/docs/source/methods.rst +++ b/docs/source/methods.rst @@ -1,32 +1,74 @@ Methods ======= -This document describes the methods and algorithms used in the `lpy_treesim` project. +`lpy_treesim` marries biologically inspired L-systems with rule-based care +operations (tying, pruning, support placement) so you can synthesize +orchard-ready tree geometries. This page explains how the major pieces interact. -L-System for Tree Generation ----------------------------- +Prototype-driven L-System +------------------------- -`lpy_treesim` uses the L-Py language to define the growth rules of the trees. L-Py is a Python-based implementation of L-systems, which are a type of formal grammar that can be used to model the growth of plants and other biological systems. +`base_lpy.lpy` is the only grammar we execute at runtime. Rather than handcode +every rule, it reads extern variables that point to Python classes in +`examples/`: -The L-system used in `lpy_treesim` is defined in the `.lpy` files in the `examples` directory. These files contain the axiom and production rules that determine the structure of the tree. +``prototype_dict_path`` + Imports the shared `basicwood_prototypes` mapping. Each entry contains a + `TreeBranch` subclass plus its `BasicWoodConfig`, which exposes stochastic + bending, bud spacing, lengths, and colors. -- **Axiom**: The axiom is the initial state of the L-system. It is a string of symbols that represents the starting point of the tree. -- **Production Rules**: The production rules are a set of rules that specify how the symbols in the L-system are replaced over time. Each rule consists of a predecessor and a successor. The predecessor is a symbol that is replaced by the successor. +``trunk_class_path`` + The entry class that seeds the axiom. All other branches sprout from the + prototypes configured on this trunk. -By iteratively applying the production rules to the axiom, the L-system generates a sequence of strings that represents the growth of the tree. +``simulation_class_path`` / ``simulation_config_class_path`` + Provide training-system-specific behaviors (e.g., UFO trellis vs. Envy + spindle). The config dataclass is serialized and handed to the runtime + simulation via `TreeSimulationBase`. -Pruning and Tying Algorithms ----------------------------- +During each derivation step the grammar delegates to your Python classes to +decide when buds break, which prototype to spawn, and how to orient each new +segment. Because everything happens inside Python, you can use NumPy, random +sampling, and heuristics tailored to your target cultivar. -`lpy_treesim` includes algorithms for pruning and tying the branches of the tree. These algorithms are used to control the shape and size of the tree. +Tying, pruning, and supports +--------------------------- -- **Pruning**: The pruning algorithm removes branches from the tree that are too long or that are growing in the wrong direction. This is done by defining a set of rules that specify which branches to remove. -- **Tying**: The tying algorithm connects branches of the tree together. This is done by defining a set of rules that specify which branches to connect and how to connect them. +The base simulation loop interleaves three routines: -The pruning and tying algorithms are implemented in the `stochastic_tree.py` module. +1. **Derive**: expand the L-system string using the prototype logic. +2. **Tie**: after a configurable number of iterations (`num_iteration_tie`), the + simulation attaches branches to the virtual support wires generated by + `TreeSimulationBase.generate_points()`. +3. **Prune**: after `num_iteration_prune`, branches that exceed thresholds (age, + curvature, or spacing) are removed. The decision functions live inside + `stochastic_tree.py` and can be augmented via your prototypes. -API Reference -------------- +All three phases reuse the `ColorManager` so instance labels remain stable even +after pruning removes geometry. -.. automodule:: stochastic_tree - :members: \ No newline at end of file +Batch export pipeline +--------------------- + +`tree_generation/make_n_trees.py` wraps the grammar in a CLI tool: + +* Builds a fresh `Lsystem` object per tree and injects the extern dictionary. +* Seeds Python's RNG per tree so stochastic decisions differ while still being + reproducible when `--rng-seed` is provided. +* Calls `sceneInterpretation` to convert the final string into a PlantGL scene. +* Writes the mesh using `tree_generation.helpers.write`, which emits ASCII PLY + with normals and color attributes. +* Dumps `{namespace}_{tree_name}_{index:05d}_colors.json` containing the mapping + from instance IDs to semantic roles (spur, branch, trunk, tie, etc.). + +The naming scheme caps at 99,999 trees per run; start a new namespace if you +need more. + +Extending the system +-------------------- + +To add a brand-new method (new pruning heuristic, new tie behavior), derive from +the appropriate class in `simulation_base.py` or `stochastic_tree.py` and inject +your class via the extern dictionary. Because all extern paths are strings, +`make_n_trees.py` can consume any importable module without further code +changes. \ No newline at end of file diff --git a/docs/source/resources.rst b/docs/source/resources.rst index ab02936..d84bd9b 100644 --- a/docs/source/resources.rst +++ b/docs/source/resources.rst @@ -1,23 +1,29 @@ Resources ========= -This page provides a list of resources related to the `lpy_treesim` project. +This section collects official links plus background material on L-systems and +training systems referenced by `lpy_treesim`. -Project Links +Project links ------------- -- **GitHub Repository**: `https://github.com/your-username/lpy_treesim `_ -- **Documentation**: `https://lpy_treesim.readthedocs.io/en/latest/ `_ +- **GitHub**: `https://github.com/OSUrobotics/lpy_treesim `_ +- **Issue tracker**: Use the GitHub repo to report bugs or request new tree + templates. -L-Py Resources --------------- +L-Py and Plant Modeling +----------------------- -- **L-Py Documentation**: `https://lpy.readthedocs.io/en/latest/ `_ -- **L-Py Training Material**: `https://github.com/fredboudon/lpy-training `_ -- **L-Py Paper**: `https://example.com/lpy-paper `_ (Replace with the actual link to the L-Py paper) +- **L-Py documentation**: `https://lpy.readthedocs.io/en/latest/ `_ +- **OpenAlea/PlantGL**: `https://openalea.readthedocs.io/en/latest/ `_ +- **L-Py training notebooks** (official tutorials): + `https://github.com/fredboudon/lpy-training `_ +- **Foundational paper**: Prusinkiewicz & Lindenmayer, *The Algorithmic Beauty + of Plants*, for a deep dive into L-systems. -Other Resources ---------------- +Supporting tooling +------------------ -- **Conda Documentation**: `https://docs.conda.io/en/latest/ `_ -- **Sphinx Documentation**: `https://www.sphinx-doc.org/en/master/ `_ \ No newline at end of file +- **Conda documentation**: `https://docs.conda.io/en/latest/ `_ +- **Sphinx documentation**: `https://www.sphinx-doc.org/en/master/ `_ +- **PLY file format**: `http://paulbourke.net/dataformats/ply/ `_ (useful when post-processing meshes). \ No newline at end of file diff --git a/docs/source/usage.rst b/docs/source/usage.rst index a1e5fc1..185ab60 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -1,71 +1,239 @@ Usage ===== -This section provides a guide on how to use `lpy_treesim` to generate and simulate tree growth. +The typical workflow for `lpy_treesim` has three stages: -Running Simulations -------------------- +1. Describe **prototypes** that capture the botanical building blocks for your + training system. +2. Provide a **simulation configuration** that tunes derivation length, pruning + passes, and support layout. +3. Feed both into the CLI generator (`tree_generation/make_n_trees.py`) to + batch-export `.ply` meshes and color maps. -To run a simulation, you need to have an L-Py file (`.lpy`) that defines the growth rules of the tree. You can find several examples in the `examples` directory. +The following sections walk through each step with concrete file references so +you can add your own tree families next to the built-in `UFO` example. -1. **Activate the Conda Environment**: +1. Define prototypes (deep dive) +-------------------------------- - .. code-block:: bash +Prototype files live under ``examples//_prototypes.py``. They +describe the biological components of your tree by subclassing +``stochastic_tree.TreeBranch`` (ultimately ``BasicWood``). Study the existing UFO +implementation for a concrete template: ``examples/UFO/UFO_prototypes.py``. - conda activate lpy +The critical building blocks are the four state dataclasses defined in +``stochastic_tree.py``: -2. **Launch L-Py**: +* ``LocationState`` tracks the start/end coordinates and the last tie point. +* ``TyingState`` stores tie axis, guide points, and the wire to attach to. +* ``GrowthState`` holds thickness increments, per-step growth length, and max + length. +* ``InfoState`` carries metadata such as age, order, prunability, and color. - .. code-block:: bash +When you instantiate a prototype with ``BasicWoodConfig`` these states are +created for you. Your subclass is responsible for overriding the behavioral +hooks: - lpy +* ``is_bud_break`` decides when a new bud/branch emerges. +* ``create_branch`` clones another prototype from ``basicwood_prototypes`` and + returns it. +* ``pre_bud_rule`` / ``post_bud_rule`` allow in-place adjustments to growth and + tying parameters. +* ``post_bud_rule`` can emit custom L-Py modules (e.g., ``@O`` for fruiting). -3. **Open an L-Py File**: In the L-Py GUI, navigate to the desired `.lpy` file and open it. +Below is a simplified excerpt from the real UFO spur definition showing how the +pieces line up: -4. **Run the Simulation**: - - Click the **Animate** button on the toolbar to start the simulation. - - **Note**: The tying and pruning processes will only work when you use **Animate**, not **Run**. - - **Warning**: There is a known bug where files may not run correctly on the first attempt. If this happens, click **Rewind** and then **Animate** again. +.. code-block:: python -Defining Growth Rules ---------------------- + from stochastic_tree import BasicWood, BasicWoodConfig -Growth rules are defined using the L-Py language in `.lpy` files. These files typically contain: + basicwood_prototypes = {} -- **Axiom**: The initial state of the L-system. -- **Productions**: A set of rules that define how the L-system evolves over time. + class Spur(TreeBranch): + def __init__(self, config=None, copy_from=None, prototype_dict=None): + super().__init__(config, copy_from, prototype_dict) -Here's a simple example of an L-Py file: + def is_bud_break(self, num_buds_segment): + if num_buds_segment >= self.growth.max_buds_segment: + return False + return rd.random() < 0.1 * (1 - num_buds_segment / self.growth.max_buds_segment) -.. code-block:: python + def create_branch(self): + return None # spurs terminate growth + + def post_bud_rule(self, plant_segment, simulation_config): + radius = plant_segment.growth.thickness * simulation_config.thickness_multiplier + return [('@O', [float(radius)])] - axiom: A(1) - production: - A(x) -> F(x) A(x+1) + spur_config = BasicWoodConfig( + max_buds_segment=2, + growth_length=0.05, + cylinder_length=0.01, + thickness=0.003, + color=[0, 255, 0], + bud_spacing_age=1, + curve_x_range=(-0.2, 0.2), + curve_y_range=(-0.2, 0.2), + curve_z_range=(-1, 1), + ) + basicwood_prototypes['spur'] = Spur(config=spur_config, prototype_dict=basicwood_prototypes) -This example defines a simple L-system that grows a branch of increasing length. +Two more classes, ``Branch`` and ``Trunk``, reference the same dictionary when +spawning children: -Visualizing the Results ------------------------ +.. code-block:: python -L-Py provides a 3D viewer that allows you to visualize the tree as it grows. You can interact with the 3D model by rotating, panning, and zooming. + class Trunk(TreeBranch): + def create_branch(self): + if rd.random() > 0.1: + return Branch(copy_from=self.prototype_dict['branch']) -Key Modules in `lpy_treesim` ----------------------------- + branch_config = BasicWoodConfig( + tie_axis=(0, 0, 1), + thickness=0.01, + thickness_increment=1e-5, + growth_length=0.1, + color=[255, 150, 0], + bud_spacing_age=2, + ) + basicwood_prototypes['branch'] = Branch(config=branch_config, prototype_dict=basicwood_prototypes) -`lpy_treesim` is organized into several modules, each with a specific purpose: +Key implementation details to replicate: -- **`stochastic_tree.py`**: This module contains the core logic for generating stochastic trees. It uses random parameters to create variations in the tree structure. -- **`helper.py`**: This module provides a set of helper functions that are used throughout the package. -- **`tree_generation/`**: This directory contains scripts for generating and converting tree models. +* Always pass ``prototype_dict=basicwood_prototypes`` when constructing each + prototype so clones reference the shared registry. +* Set ``BasicWoodConfig.tie_axis`` for the classes you expect to tie; the base + simulation will skip tying for branches whose tie axis is ``None``. +* Use ``BasicWoodConfig.color`` for per-instance labeling—the ``ColorManager`` + picks up these RGB triplets and writes them to the ``*_colors.json`` mapping. -To use these modules, you can import them into your L-Py files or other Python scripts. For example: +2. Configure simulation parameters +---------------------------------- -.. code-block:: python +The simulator pairs your prototypes with tie/prune logic by subclassing +`TreeSimulationBase` and `SimulationConfig` (see `simulation_base.py`). Each tree +family stores both classes in `examples//_simulation.py`. - from stochastic_tree import generate_stochastic_tree +For example, ``examples/UFO/UFO_simulation.py`` implements both the config and +the runtime class: - # Generate a stochastic tree with custom parameters - tree = generate_stochastic_tree(num_branches=10, branch_length=0.5) +.. code-block:: python -For more detailed examples, please refer to the scripts in the `examples` directory. + from simulation_base import SimulationConfig, TreeSimulationBase + + @dataclass + class UFOSimulationConfig(SimulationConfig): + num_iteration_tie: int = 8 + num_iteration_prune: int = 16 + pruning_age_threshold: int = 8 + derivation_length: int = 160 + support_trunk_wire_point: tuple = (0.6, 0, 0.4) + support_num_wires: int = 7 + ufo_x_range: tuple = (0.65, 3) + ufo_x_spacing: float = 0.3 + ufo_z_value: float = 1.4 + ufo_y_value: float = 0 + thickness_multiplier: float = 1.2 + semantic_label: bool = True + + class UFOSimulation(TreeSimulationBase): + def generate_points(self): + x = np.arange( + self.config.ufo_x_range[0], + self.config.ufo_x_range[1], + self.config.ufo_x_spacing, + ) + z = np.full((x.shape[0],), self.config.ufo_z_value) + y = np.full((x.shape[0],), self.config.ufo_y_value) + return list(zip(x, y, z)) + +``SimulationConfig`` enforces consistent behavior via ``__post_init__``—only one +labeling mode (semantic / instance / per-cylinder) can be true at a time. The +base class also exposes: + +* ``num_iteration_tie`` / ``num_iteration_prune``: cadence for maintenance. +* ``energy_distance_weight`` / ``energy_threshold``: scoring knobs for the + branch-to-wire assignment matrix built inside ``TreeSimulationBase.get_energy_mat``. +* ``pruning_age_threshold``: compared against ``branch.info.age`` in + ``TreeSimulationBase.prune`` before removing geometry via ``helper.cut_from``. + +On the runtime side, ``TreeSimulationBase`` supplies ready-to-use algorithms for +tying, pruning, and support assignment: + +* ``generate_points`` must return the actual wire coordinates used when tie + curves are computed (``BasicWood.update_guide``). +* ``tie`` walks the L-system string and calls ``branch.tie_lstring`` for one + eligible branch per invocation. +* ``prune`` removes untied branches whose age exceeds + ``config.pruning_age_threshold`` and whose prototype flag ``prunable`` is set. + +To bring up a new architecture, duplicate the UFO module, rename the classes to +``SimulationConfig`` / ``Simulation``, and add any extra +dataclass fields required for your geometry (wire spacing, tie axis overrides, +etc.). Ensure the class names match the paths you pass to ``make_n_trees.py``. + +Checklist for a new tree type: + +1. Copy `examples/UFO/UFO_simulation.py` to `examples//_simulation.py`. +2. Rename the dataclass to `SimulationConfig`. +3. Rename the runtime class to `Simulation` and override any helper + methods you need. +4. Ensure the module exposes the two symbols with those exact names so the CLI + resolver can import them. + +3. Batch-generate assets +------------------------ + +Once prototypes and simulations exist, the CLI script assembles everything. It +always loads `base_lpy.lpy` and expects your modules to live inside the +`examples` package. + +.. code-block:: bash + + cd lpy_treesim + python lpy_treesim/tree_generation/make_n_trees.py \ + --tree_name UFO \ + --namespace orchardA \ + --num_trees 64 \ + --output_dir dataset/ufo_batch \ + --rng-seed 42 \ + --verbose + +Important flags: + +``--tree_name`` + The directory under `examples/` that contains both the prototype and + simulation modules (`examples/UFO`, `examples/Envy`, etc.). The script + automatically builds module paths such as + `examples.UFO.UFO_prototypes.basicwood_prototypes`. + +``--namespace`` + Prefix for exported files. Meshes are named + ``{namespace}_{tree_name}_{index:05d}.ply`` and color maps are suffixed with + ``_colors.json``. Up to 99,999 indices are supported per run. + +``--rng-seed`` + Provides reproducible randomness while still using a different seed for each + tree inside the batch. + +Outputs include: + +- `.ply` meshes stored in the target directory. +- JSON color maps emitted by `ColorManager` so downstream segmentation models + can recover per-instance labeling. + +4. Inspect results (optional) +----------------------------- + +If you want to watch an individual tree evolve, run the same environment through +the L-Py GUI: + +.. code-block:: bash + + conda activate lpy + lpy lpy_treesim/base_lpy.lpy + +Inside the GUI, set the extern variables (prototype paths, simulation classes, +`color_manager`, etc.) to match the CLI defaults or a custom configuration +dictionary. Use **Animate** rather than **Run** so tying/pruning hooks fire.